diff --git a/src/components/favicons/group.jsx b/src/components/favicons/group.jsx
new file mode 100644
index 00000000..2b4bb5e4
--- /dev/null
+++ b/src/components/favicons/group.jsx
@@ -0,0 +1,77 @@
+import { useRef, useEffect } from "react";
+import classNames from "classnames";
+import { Disclosure, Transition } from "@headlessui/react";
+import { MdKeyboardArrowDown } from "react-icons/md";
+
+import ErrorBoundary from "components/errorboundry";
+import List from "components/favicons/list";
+import ResolvedIcon from "components/resolvedicon";
+
+export default function FaviconsGroup({ favicons, layout, disableCollapse, groupsInitiallyCollapsed }) {
+ const panel = useRef();
+ console.log('+++++')
+ console.log(favicons)
+ useEffect(() => {
+ if (layout?.initiallyCollapsed ?? groupsInitiallyCollapsed) panel.current.style.height = `0`;
+ }, [layout, groupsInitiallyCollapsed]);
+
+ return (
+
+
+ {({ open }) => (
+ <>
+ {layout?.header !== false && (
+
+ {layout?.icon && (
+
+
+
+ )}
+
+ {favicons.name}
+
+
+
+ )}
+ {
+ panel.current.style.height = `${panel.current.scrollHeight}px`;
+ setTimeout(() => {
+ panel.current.style.height = `0`;
+ }, 1);
+ }}
+ beforeEnter={() => {
+ panel.current.style.height = `0px`;
+ setTimeout(() => {
+ panel.current.style.height = `${panel.current.scrollHeight}px`;
+ }, 1);
+ }}
+ >
+
+
+
+
+
+
+ >
+ )}
+
+
+ );
+}
diff --git a/src/components/favicons/item.jsx b/src/components/favicons/item.jsx
new file mode 100644
index 00000000..77aa806f
--- /dev/null
+++ b/src/components/favicons/item.jsx
@@ -0,0 +1,33 @@
+import { useContext } from "react";
+import classNames from "classnames";
+
+import { SettingsContext } from "utils/contexts/settings";
+import ResolvedIcon from "components/resolvedicon";
+
+export default function Item({ favicon }) {
+ const description = favicon.description ?? new URL(favicon.href).hostname;
+ const { settings } = useContext(SettingsContext);
+
+ return (
+
+
+
+ {favicon.icon && (
+
+ )}
+ {!favicon.icon && favicon.abbr}
+
+
+
+
+ );
+}
diff --git a/src/components/favicons/list.jsx b/src/components/favicons/list.jsx
new file mode 100644
index 00000000..021eda51
--- /dev/null
+++ b/src/components/favicons/list.jsx
@@ -0,0 +1,17 @@
+import classNames from "classnames";
+
+import { columnMap } from "../../utils/layout/columns";
+
+import Item from "components/favicons/item";
+
+export default function List({ favicons, layout }) {
+ return (
+
+ {favicons.map((favicon) => (
+
+ ))}
+
+ );
+}
diff --git a/src/pages/api/favicons.js b/src/pages/api/favicons.js
new file mode 100644
index 00000000..8ca752b2
--- /dev/null
+++ b/src/pages/api/favicons.js
@@ -0,0 +1,5 @@
+import { faviconsResponse } from "utils/config/api-response";
+
+export default async function handler(req, res) {
+ res.send(await faviconsResponse());
+}
diff --git a/src/pages/api/hash.js b/src/pages/api/hash.js
index 992f9ea6..3a48a7a7 100644
--- a/src/pages/api/hash.js
+++ b/src/pages/api/hash.js
@@ -9,6 +9,7 @@ const configs = [
"settings.yaml",
"services.yaml",
"bookmarks.yaml",
+ "favicons.yaml",
"widgets.yaml",
"custom.css",
"custom.js",
diff --git a/src/pages/api/validate.js b/src/pages/api/validate.js
index bab53057..ae0526cf 100644
--- a/src/pages/api/validate.js
+++ b/src/pages/api/validate.js
@@ -1,6 +1,6 @@
import checkAndCopyConfig from "utils/config/config";
-const configs = ["docker.yaml", "settings.yaml", "services.yaml", "bookmarks.yaml"];
+const configs = ["docker.yaml", "favicons.yaml", "settings.yaml", "services.yaml", "bookmarks.yaml"];
export default async function handler(req, res) {
const errors = configs.map((config) => checkAndCopyConfig(config)).filter((status) => status !== true);
diff --git a/src/pages/index.jsx b/src/pages/index.jsx
index dd0df95f..4b019bdc 100644
--- a/src/pages/index.jsx
+++ b/src/pages/index.jsx
@@ -13,6 +13,7 @@ import { useRouter } from "next/router";
import Tab, { slugifyAndEncode } from "components/tab";
import ServicesGroup from "components/services/group";
import BookmarksGroup from "components/bookmarks/group";
+import FaviconsGroup from "components/favicons/group";
import Widget from "components/widgets/widget";
import Revalidate from "components/toggles/revalidate";
import createLogger from "utils/logger";
@@ -22,7 +23,7 @@ import { ColorContext } from "utils/contexts/color";
import { ThemeContext } from "utils/contexts/theme";
import { SettingsContext } from "utils/contexts/settings";
import { TabContext } from "utils/contexts/tab";
-import { bookmarksResponse, servicesResponse, widgetsResponse } from "utils/config/api-response";
+import { bookmarksResponse, faviconsResponse, servicesResponse, widgetsResponse } from "utils/config/api-response";
import ErrorBoundary from "components/errorboundry";
import themes from "utils/styles/themes";
import QuickLaunch from "components/quicklaunch";
@@ -49,12 +50,14 @@ export async function getStaticProps() {
const services = await servicesResponse();
const bookmarks = await bookmarksResponse();
+ const favicons = await faviconsResponse();
const widgets = await widgetsResponse();
return {
props: {
initialSettings: settings,
fallback: {
+ "/api/favicons": favicons,
"/api/services": services,
"/api/bookmarks": bookmarks,
"/api/widgets": widgets,
@@ -71,6 +74,7 @@ export async function getStaticProps() {
props: {
initialSettings: {},
fallback: {
+ "/api/favicons": [],
"/api/services": [],
"/api/bookmarks": [],
"/api/widgets": [],
@@ -180,11 +184,13 @@ function Home({ initialSettings }) {
const { data: services } = useSWR("/api/services");
const { data: bookmarks } = useSWR("/api/bookmarks");
+ const { data: favicons } = useSWR("/api/favicons");
const { data: widgets } = useSWR("/api/widgets");
const servicesAndBookmarks = [
...services.map((sg) => sg.services).flat(),
...bookmarks.map((bg) => bg.bookmarks).flat(),
+ ...favicons.map((fg) => fg.favicons).flat(),
].filter((i) => i?.href);
useEffect(() => {
@@ -254,7 +260,7 @@ function Home({ initialSettings }) {
const undefinedGroupFilter = (g) => settings.layout?.[g.name] === undefined;
const layoutGroups = Object.keys(settings.layout ?? {})
- .map((groupName) => services?.find((g) => g.name === groupName) ?? bookmarks?.find((b) => b.name === groupName))
+ .map((groupName) => services?.find((g) => g.name === groupName) ?? favicons?.find((f) => f.name === groupName) ?? bookmarks?.find((b) => b.name === groupName) )
.filter(tabGroupFilter);
if (!settings.layout && JSON.stringify(settings.layout) !== JSON.stringify(initialSettings.layout)) {
@@ -264,7 +270,10 @@ function Home({ initialSettings }) {
const serviceGroups = services?.filter(tabGroupFilter).filter(undefinedGroupFilter);
const bookmarkGroups = bookmarks.filter(tabGroupFilter).filter(undefinedGroupFilter);
-
+ const faviconGroups = favicons.filter(tabGroupFilter).filter(undefinedGroupFilter);
+ console.log(bookmarkGroups)
+ console.log('------')
+ console.log(faviconGroups)
return (
<>
{tabs.length > 0 && (
@@ -300,6 +309,7 @@ function Home({ initialSettings }) {
groupsInitiallyCollapsed={settings.groupsInitiallyCollapsed}
/>
) : (
+ <>
+
+ >
),
)}
@@ -339,6 +357,19 @@ function Home({ initialSettings }) {
))}
)}
+ {faviconGroups?.length > 0 && (
+
+ {faviconGroups.map((group) => (
+
+ ))}
+
+ )}
>
);
}, [
@@ -346,6 +377,7 @@ function Home({ initialSettings }) {
activeTab,
services,
bookmarks,
+ favicons,
settings.layout,
settings.fiveColumns,
settings.disableCollapse,
diff --git a/src/styles/globals.css b/src/styles/globals.css
index f3bfec78..034594f7 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -25,6 +25,12 @@ body {
padding: 0;
}
+.favicon {
+ display: inline-block;
+ margin-right: 0.5em;
+}
+
+
.light {
--bg-color: var(--color-50);
--scrollbar-thumb: rgb(var(--color-300));
diff --git a/src/utils/config/api-response.js b/src/utils/config/api-response.js
index 97f61ea0..8eb3c1a4 100644
--- a/src/utils/config/api-response.js
+++ b/src/utils/config/api-response.js
@@ -24,6 +24,54 @@ function compareServices(service1, service2) {
return service1.name.localeCompare(service2.name);
}
+
+export async function faviconsResponse() {
+ checkAndCopyConfig("favicons.yaml");
+
+ const faviconsYaml = path.join(CONF_DIR, "favicons.yaml");
+ const rawFileContents = await fs.readFile(faviconsYaml, "utf8");
+ const fileContents = substituteEnvironmentVars(rawFileContents);
+ const favicons = yaml.load(fileContents);
+
+ if (!favicons) return [];
+
+ let initialSettings;
+
+ try {
+ initialSettings = await getSettings();
+ } catch (e) {
+ console.error("Failed to load favicons.yaml, please check for errors");
+ if (e) console.error(e.toString());
+ initialSettings = {};
+ }
+
+ // map easy to write YAML objects into easy to consume JS arrays
+ const faviconsArray = favicons.map((group) => ({
+ name: Object.keys(group)[0],
+ bookmarks: group[Object.keys(group)[0]].map((entries) => ({
+ name: Object.keys(entries)[0],
+ ...entries[Object.keys(entries)[0]][0],
+ })),
+ }));
+
+ const sortedGroups = [];
+ const unsortedGroups = [];
+ const definedLayouts = initialSettings.layout ? Object.keys(initialSettings.layout) : null;
+
+ faviconsArray.forEach((group) => {
+ if (definedLayouts) {
+ const layoutIndex = definedLayouts.findIndex((layout) => layout === group.name);
+ if (layoutIndex > -1) sortedGroups[layoutIndex] = group;
+ else unsortedGroups.push(group);
+ } else {
+ unsortedGroups.push(group);
+ }
+ });
+
+ return [...sortedGroups.filter((g) => g), ...unsortedGroups];
+}
+
+
export async function bookmarksResponse() {
checkAndCopyConfig("bookmarks.yaml");