add favicons component

This commit is contained in:
Jonny McCullagh 2024-11-15 14:15:58 +00:00
parent e730a0ceb0
commit 8a25f25e3c
9 changed files with 223 additions and 4 deletions

View File

@ -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 (
<div
key={favicons.name}
className={classNames(
"favicon-group",
layout?.style === "row" ? "basis-full" : "basis-full md:basis-1/4 lg:basis-1/5 xl:basis-1/6",
layout?.header === false ? "flex-1 px-1 -my-1 overflow-hidden" : "flex-1 p-1 overflow-hidden",
)}
>
<Disclosure defaultOpen={!(layout?.initiallyCollapsed ?? groupsInitiallyCollapsed) ?? true}>
{({ open }) => (
<>
{layout?.header !== false && (
<Disclosure.Button disabled={disableCollapse} className="flex w-full select-none items-center group">
{layout?.icon && (
<div className="flex-shrink-0 mr-2 w-7 h-7 favicon-group-icon">
<ResolvedIcon icon={layout.icon} />
</div>
)}
<h2 className="text-theme-800 dark:text-theme-300 text-xl font-medium favicon-group-name">
{favicons.name}
</h2>
<MdKeyboardArrowDown
className={classNames(
disableCollapse ? "hidden" : "",
"transition-all opacity-0 group-hover:opacity-100 ml-auto text-theme-800 dark:text-theme-300 text-xl",
open ? "" : "rotate-180",
)}
/>
</Disclosure.Button>
)}
<Transition
// Otherwise the transition group does display: none and cancels animation
className="!block"
unmount={false}
beforeLeave={() => {
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);
}}
>
<Disclosure.Panel className="transition-all overflow-hidden duration-300 ease-out" ref={panel} static>
<ErrorBoundary>
<List favicons={favicons.bookmarks} layout={layout} />
</ErrorBoundary>
</Disclosure.Panel>
</Transition>
</>
)}
</Disclosure>
</div>
);
}

View File

@ -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 (
<li key={favicon.name} id={favicon.id} className="favicon" data-name={favicon.name}>
<a
href={favicon.href}
title={favicon.name}
rel="noreferrer"
target={favicon.target ?? settings.target ?? "_blank"}
className={classNames(
settings.cardBlur !== undefined && `backdrop-blur${settings.cardBlur.length ? "-" : ""}${settings.cardBlur}`,
"",
)}
>
<div className="">
{favicon.icon && (
<ResolvedIcon icon={favicon.icon} alt={favicon.abbr} />
)}
{!favicon.icon && favicon.abbr}
</div>
</a>
</li>
);
}

View File

@ -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 (
<ul
className="favicon-list"
>
{favicons.map((favicon) => (
<Item key={`${favicon.name}-${favicon.href}`} favicon={favicon} />
))}
</ul>
);
}

View File

@ -0,0 +1,5 @@
import { faviconsResponse } from "utils/config/api-response";
export default async function handler(req, res) {
res.send(await faviconsResponse());
}

View File

@ -9,6 +9,7 @@ const configs = [
"settings.yaml", "settings.yaml",
"services.yaml", "services.yaml",
"bookmarks.yaml", "bookmarks.yaml",
"favicons.yaml",
"widgets.yaml", "widgets.yaml",
"custom.css", "custom.css",
"custom.js", "custom.js",

View File

@ -1,6 +1,6 @@
import checkAndCopyConfig from "utils/config/config"; 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) { export default async function handler(req, res) {
const errors = configs.map((config) => checkAndCopyConfig(config)).filter((status) => status !== true); const errors = configs.map((config) => checkAndCopyConfig(config)).filter((status) => status !== true);

View File

@ -13,6 +13,7 @@ import { useRouter } from "next/router";
import Tab, { slugifyAndEncode } from "components/tab"; import Tab, { slugifyAndEncode } from "components/tab";
import ServicesGroup from "components/services/group"; import ServicesGroup from "components/services/group";
import BookmarksGroup from "components/bookmarks/group"; import BookmarksGroup from "components/bookmarks/group";
import FaviconsGroup from "components/favicons/group";
import Widget from "components/widgets/widget"; import Widget from "components/widgets/widget";
import Revalidate from "components/toggles/revalidate"; import Revalidate from "components/toggles/revalidate";
import createLogger from "utils/logger"; import createLogger from "utils/logger";
@ -22,7 +23,7 @@ import { ColorContext } from "utils/contexts/color";
import { ThemeContext } from "utils/contexts/theme"; import { ThemeContext } from "utils/contexts/theme";
import { SettingsContext } from "utils/contexts/settings"; import { SettingsContext } from "utils/contexts/settings";
import { TabContext } from "utils/contexts/tab"; 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 ErrorBoundary from "components/errorboundry";
import themes from "utils/styles/themes"; import themes from "utils/styles/themes";
import QuickLaunch from "components/quicklaunch"; import QuickLaunch from "components/quicklaunch";
@ -49,12 +50,14 @@ export async function getStaticProps() {
const services = await servicesResponse(); const services = await servicesResponse();
const bookmarks = await bookmarksResponse(); const bookmarks = await bookmarksResponse();
const favicons = await faviconsResponse();
const widgets = await widgetsResponse(); const widgets = await widgetsResponse();
return { return {
props: { props: {
initialSettings: settings, initialSettings: settings,
fallback: { fallback: {
"/api/favicons": favicons,
"/api/services": services, "/api/services": services,
"/api/bookmarks": bookmarks, "/api/bookmarks": bookmarks,
"/api/widgets": widgets, "/api/widgets": widgets,
@ -71,6 +74,7 @@ export async function getStaticProps() {
props: { props: {
initialSettings: {}, initialSettings: {},
fallback: { fallback: {
"/api/favicons": [],
"/api/services": [], "/api/services": [],
"/api/bookmarks": [], "/api/bookmarks": [],
"/api/widgets": [], "/api/widgets": [],
@ -180,11 +184,13 @@ function Home({ initialSettings }) {
const { data: services } = useSWR("/api/services"); const { data: services } = useSWR("/api/services");
const { data: bookmarks } = useSWR("/api/bookmarks"); const { data: bookmarks } = useSWR("/api/bookmarks");
const { data: favicons } = useSWR("/api/favicons");
const { data: widgets } = useSWR("/api/widgets"); const { data: widgets } = useSWR("/api/widgets");
const servicesAndBookmarks = [ const servicesAndBookmarks = [
...services.map((sg) => sg.services).flat(), ...services.map((sg) => sg.services).flat(),
...bookmarks.map((bg) => bg.bookmarks).flat(), ...bookmarks.map((bg) => bg.bookmarks).flat(),
...favicons.map((fg) => fg.favicons).flat(),
].filter((i) => i?.href); ].filter((i) => i?.href);
useEffect(() => { useEffect(() => {
@ -254,7 +260,7 @@ function Home({ initialSettings }) {
const undefinedGroupFilter = (g) => settings.layout?.[g.name] === undefined; const undefinedGroupFilter = (g) => settings.layout?.[g.name] === undefined;
const layoutGroups = Object.keys(settings.layout ?? {}) 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); .filter(tabGroupFilter);
if (!settings.layout && JSON.stringify(settings.layout) !== JSON.stringify(initialSettings.layout)) { 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 serviceGroups = services?.filter(tabGroupFilter).filter(undefinedGroupFilter);
const bookmarkGroups = bookmarks.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 ( return (
<> <>
{tabs.length > 0 && ( {tabs.length > 0 && (
@ -300,6 +309,7 @@ function Home({ initialSettings }) {
groupsInitiallyCollapsed={settings.groupsInitiallyCollapsed} groupsInitiallyCollapsed={settings.groupsInitiallyCollapsed}
/> />
) : ( ) : (
<>
<BookmarksGroup <BookmarksGroup
key={group.name} key={group.name}
bookmarks={group} bookmarks={group}
@ -307,6 +317,14 @@ function Home({ initialSettings }) {
disableCollapse={settings.disableCollapse} disableCollapse={settings.disableCollapse}
groupsInitiallyCollapsed={settings.groupsInitiallyCollapsed} groupsInitiallyCollapsed={settings.groupsInitiallyCollapsed}
/> />
<FaviconsGroup
key={group.name}
bookmarks={group}
layout={settings.layout?.[group.name]}
disableCollapse={settings.disableCollapse}
groupsInitiallyCollapsed={settings.groupsInitiallyCollapsed}
/>
</>
), ),
)} )}
</div> </div>
@ -339,6 +357,19 @@ function Home({ initialSettings }) {
))} ))}
</div> </div>
)} )}
{faviconGroups?.length > 0 && (
<div key="favicons" id="favicons" className="flex flex-wrap m-4 sm:m-8 sm:mt-4 items-start mb-2">
{faviconGroups.map((group) => (
<FaviconsGroup
key={group.name}
favicons={group}
layout={settings.layout?.[group.name]}
disableCollapse={settings.disableCollapse}
groupsInitiallyCollapsed={settings.groupsInitiallyCollapsed}
/>
))}
</div>
)}
</> </>
); );
}, [ }, [
@ -346,6 +377,7 @@ function Home({ initialSettings }) {
activeTab, activeTab,
services, services,
bookmarks, bookmarks,
favicons,
settings.layout, settings.layout,
settings.fiveColumns, settings.fiveColumns,
settings.disableCollapse, settings.disableCollapse,

View File

@ -25,6 +25,12 @@ body {
padding: 0; padding: 0;
} }
.favicon {
display: inline-block;
margin-right: 0.5em;
}
.light { .light {
--bg-color: var(--color-50); --bg-color: var(--color-50);
--scrollbar-thumb: rgb(var(--color-300)); --scrollbar-thumb: rgb(var(--color-300));

View File

@ -24,6 +24,54 @@ function compareServices(service1, service2) {
return service1.name.localeCompare(service2.name); 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() { export async function bookmarksResponse() {
checkAndCopyConfig("bookmarks.yaml"); checkAndCopyConfig("bookmarks.yaml");