add favicons component
This commit is contained in:
parent
e730a0ceb0
commit
8a25f25e3c
77
src/components/favicons/group.jsx
Normal file
77
src/components/favicons/group.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
33
src/components/favicons/item.jsx
Normal file
33
src/components/favicons/item.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
src/components/favicons/list.jsx
Normal file
17
src/components/favicons/list.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
src/pages/api/favicons.js
Normal file
5
src/pages/api/favicons.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { faviconsResponse } from "utils/config/api-response";
|
||||||
|
|
||||||
|
export default async function handler(req, res) {
|
||||||
|
res.send(await faviconsResponse());
|
||||||
|
}
|
||||||
@ -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",
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
@ -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");
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user