feat: dynamic favicon (#11603)

* feat: dynamic favicon

* feat: dynamic favicon

* feat: dynamic favicon

* feat: dynamic favicon
This commit is contained in:
René Wang
2026-01-19 15:59:39 +08:00
committed by GitHub
parent 2f032d44d1
commit d92550cbd2
14 changed files with 107 additions and 11 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
public/favicon-done-dev.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 B

BIN
public/favicon-done.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 914 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 837 B

BIN
public/favicon-error.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 901 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 828 B

BIN
public/favicon-progress.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 908 B

View 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';

View File

@@ -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}