feat: dynamic favicon (#11603)
* feat: dynamic favicon * feat: dynamic favicon * feat: dynamic favicon * feat: dynamic favicon
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 835 B |
|
After Width: | Height: | Size: 914 B |
|
After Width: | Height: | Size: 837 B |
|
After Width: | Height: | Size: 901 B |
|
After Width: | Height: | Size: 828 B |
|
After Width: | Height: | Size: 908 B |
@@ -0,0 +1,92 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { type ReactNode, createContext, memo, useCallback, useContext, useState } from 'react';
|
||||||
|
|
||||||
|
export type FaviconState = 'default' | 'done' | 'error' | 'progress';
|
||||||
|
|
||||||
|
interface FaviconContextValue {
|
||||||
|
currentState: FaviconState;
|
||||||
|
isDevMode: boolean;
|
||||||
|
setFavicon: (state: FaviconState) => void;
|
||||||
|
setIsDevMode: (isDev: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FaviconContext = createContext<FaviconContextValue | null>(null);
|
||||||
|
|
||||||
|
export const useFavicon = () => {
|
||||||
|
const context = useContext(FaviconContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useFavicon must be used within FaviconProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
const stateToFileName: Record<FaviconState, string> = {
|
||||||
|
default: '',
|
||||||
|
done: '-done',
|
||||||
|
error: '-error',
|
||||||
|
progress: '-progress',
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFaviconPath = (state: FaviconState, isDev: boolean, size?: '32x32'): string => {
|
||||||
|
const devSuffix = isDev ? '-dev' : '';
|
||||||
|
const stateSuffix = stateToFileName[state];
|
||||||
|
const sizeSuffix = size ? `-${size}` : '';
|
||||||
|
return `/favicon${sizeSuffix}${stateSuffix}${devSuffix}.ico`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateFaviconDOM = (state: FaviconState, isDev: boolean) => {
|
||||||
|
if (typeof document === 'undefined') return;
|
||||||
|
|
||||||
|
const head = document.head;
|
||||||
|
const existingLinks = document.querySelectorAll<HTMLLinkElement>(
|
||||||
|
'link[rel="icon"], link[rel="shortcut icon"]',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove existing favicon links and create new ones to bust cache
|
||||||
|
existingLinks.forEach((link) => {
|
||||||
|
const oldHref = link.href;
|
||||||
|
const is32 = oldHref.includes('32x32');
|
||||||
|
const rel = link.rel;
|
||||||
|
|
||||||
|
// Remove old link
|
||||||
|
link.remove();
|
||||||
|
|
||||||
|
// Create new link with cache-busting query param
|
||||||
|
const newLink = document.createElement('link');
|
||||||
|
newLink.rel = rel;
|
||||||
|
newLink.href = `${getFaviconPath(state, isDev, is32 ? '32x32' : undefined)}?v=${Date.now()}`;
|
||||||
|
head.append(newLink);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultIsDev = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
|
export const FaviconProvider = memo<{ children: ReactNode }>(({ children }) => {
|
||||||
|
const [currentState, setCurrentState] = useState<FaviconState>('default');
|
||||||
|
const [isDevMode, setIsDevModeState] = useState<boolean>(defaultIsDev);
|
||||||
|
|
||||||
|
const setFavicon = useCallback(
|
||||||
|
(state: FaviconState) => {
|
||||||
|
setCurrentState(state);
|
||||||
|
updateFaviconDOM(state, isDevMode);
|
||||||
|
},
|
||||||
|
[isDevMode],
|
||||||
|
);
|
||||||
|
|
||||||
|
const setIsDevMode = useCallback(
|
||||||
|
(isDev: boolean) => {
|
||||||
|
setIsDevModeState(isDev);
|
||||||
|
updateFaviconDOM(currentState, isDev);
|
||||||
|
},
|
||||||
|
[currentState],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FaviconContext.Provider value={{ currentState, isDevMode, setFavicon, setIsDevMode }}>
|
||||||
|
{children}
|
||||||
|
</FaviconContext.Provider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
FaviconProvider.displayName = 'FaviconProvider';
|
||||||
@@ -14,6 +14,7 @@ import { ServerConfigStoreProvider } from '@/store/serverConfig/Provider';
|
|||||||
import { getAntdLocale } from '@/utils/locale';
|
import { getAntdLocale } from '@/utils/locale';
|
||||||
|
|
||||||
import AppTheme from './AppTheme';
|
import AppTheme from './AppTheme';
|
||||||
|
import { FaviconProvider } from './FaviconProvider';
|
||||||
import { GroupWizardProvider } from './GroupWizardProvider';
|
import { GroupWizardProvider } from './GroupWizardProvider';
|
||||||
import ImportSettings from './ImportSettings';
|
import ImportSettings from './ImportSettings';
|
||||||
import Locale from './Locale';
|
import Locale from './Locale';
|
||||||
@@ -65,17 +66,20 @@ const GlobalLayout = async ({
|
|||||||
>
|
>
|
||||||
<QueryProvider>
|
<QueryProvider>
|
||||||
<StoreInitialization />
|
<StoreInitialization />
|
||||||
<GroupWizardProvider>
|
<FaviconProvider>
|
||||||
<DragUploadProvider>
|
{/* {process.env.NODE_ENV === 'development' && <FaviconTestPanel />} */}
|
||||||
<LazyMotion features={domMax}>
|
<GroupWizardProvider>
|
||||||
<TooltipGroup layoutAnimation={false}>
|
<DragUploadProvider>
|
||||||
<LobeAnalyticsProviderWrapper>{children}</LobeAnalyticsProviderWrapper>
|
<LazyMotion features={domMax}>
|
||||||
</TooltipGroup>
|
<TooltipGroup layoutAnimation={false}>
|
||||||
<ModalHost />
|
<LobeAnalyticsProviderWrapper>{children}</LobeAnalyticsProviderWrapper>
|
||||||
<ContextMenuHost />
|
</TooltipGroup>
|
||||||
</LazyMotion>
|
<ModalHost />
|
||||||
</DragUploadProvider>
|
<ContextMenuHost />
|
||||||
</GroupWizardProvider>
|
</LazyMotion>
|
||||||
|
</DragUploadProvider>
|
||||||
|
</GroupWizardProvider>
|
||||||
|
</FaviconProvider>
|
||||||
</QueryProvider>
|
</QueryProvider>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
{ENABLE_BUSINESS_FEATURES ? <ReferralProvider /> : null}
|
{ENABLE_BUSINESS_FEATURES ? <ReferralProvider /> : null}
|
||||||
|
|||||||