feat: dynamic favicon (#11603)
* feat: dynamic favicon * feat: dynamic favicon * feat: dynamic favicon * feat: dynamic favicon
BIN
public/favicon-32x-32-error.ico
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
public/favicon-32x32-done-dev.ico
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
public/favicon-32x32-done.ico
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
public/favicon-32x32-error-dev.ico
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
public/favicon-32x32-progress-dev.ico
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
public/favicon-32x32-progress.ico
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
public/favicon-done-dev.ico
Normal file
|
After Width: | Height: | Size: 835 B |
BIN
public/favicon-done.ico
Normal file
|
After Width: | Height: | Size: 914 B |
BIN
public/favicon-error-dev.ico
Normal file
|
After Width: | Height: | Size: 837 B |
BIN
public/favicon-error.ico
Normal file
|
After Width: | Height: | Size: 901 B |
BIN
public/favicon-progress-dev.ico
Normal file
|
After Width: | Height: | Size: 828 B |
BIN
public/favicon-progress.ico
Normal file
|
After Width: | Height: | Size: 908 B |
92
src/layout/GlobalProvider/FaviconProvider.tsx
Normal file
@@ -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 AppTheme from './AppTheme';
|
||||
import { FaviconProvider } from './FaviconProvider';
|
||||
import { GroupWizardProvider } from './GroupWizardProvider';
|
||||
import ImportSettings from './ImportSettings';
|
||||
import Locale from './Locale';
|
||||
@@ -65,17 +66,20 @@ const GlobalLayout = async ({
|
||||
>
|
||||
<QueryProvider>
|
||||
<StoreInitialization />
|
||||
<GroupWizardProvider>
|
||||
<DragUploadProvider>
|
||||
<LazyMotion features={domMax}>
|
||||
<TooltipGroup layoutAnimation={false}>
|
||||
<LobeAnalyticsProviderWrapper>{children}</LobeAnalyticsProviderWrapper>
|
||||
</TooltipGroup>
|
||||
<ModalHost />
|
||||
<ContextMenuHost />
|
||||
</LazyMotion>
|
||||
</DragUploadProvider>
|
||||
</GroupWizardProvider>
|
||||
<FaviconProvider>
|
||||
{/* {process.env.NODE_ENV === 'development' && <FaviconTestPanel />} */}
|
||||
<GroupWizardProvider>
|
||||
<DragUploadProvider>
|
||||
<LazyMotion features={domMax}>
|
||||
<TooltipGroup layoutAnimation={false}>
|
||||
<LobeAnalyticsProviderWrapper>{children}</LobeAnalyticsProviderWrapper>
|
||||
</TooltipGroup>
|
||||
<ModalHost />
|
||||
<ContextMenuHost />
|
||||
</LazyMotion>
|
||||
</DragUploadProvider>
|
||||
</GroupWizardProvider>
|
||||
</FaviconProvider>
|
||||
</QueryProvider>
|
||||
<Suspense>
|
||||
{ENABLE_BUSINESS_FEATURES ? <ReferralProvider /> : null}
|
||||
|
||||