diff --git a/public/favicon-32x-32-error.ico b/public/favicon-32x-32-error.ico new file mode 100644 index 0000000000..b7f7b85347 Binary files /dev/null and b/public/favicon-32x-32-error.ico differ diff --git a/public/favicon-32x32-done-dev.ico b/public/favicon-32x32-done-dev.ico new file mode 100644 index 0000000000..96b412b902 Binary files /dev/null and b/public/favicon-32x32-done-dev.ico differ diff --git a/public/favicon-32x32-done.ico b/public/favicon-32x32-done.ico new file mode 100644 index 0000000000..89e6d91a32 Binary files /dev/null and b/public/favicon-32x32-done.ico differ diff --git a/public/favicon-32x32-error-dev.ico b/public/favicon-32x32-error-dev.ico new file mode 100644 index 0000000000..bbb9e150a0 Binary files /dev/null and b/public/favicon-32x32-error-dev.ico differ diff --git a/public/favicon-32x32-progress-dev.ico b/public/favicon-32x32-progress-dev.ico new file mode 100644 index 0000000000..a52c687270 Binary files /dev/null and b/public/favicon-32x32-progress-dev.ico differ diff --git a/public/favicon-32x32-progress.ico b/public/favicon-32x32-progress.ico new file mode 100644 index 0000000000..2828084c35 Binary files /dev/null and b/public/favicon-32x32-progress.ico differ diff --git a/public/favicon-done-dev.ico b/public/favicon-done-dev.ico new file mode 100644 index 0000000000..0a9d15b45c Binary files /dev/null and b/public/favicon-done-dev.ico differ diff --git a/public/favicon-done.ico b/public/favicon-done.ico new file mode 100644 index 0000000000..874572697b Binary files /dev/null and b/public/favicon-done.ico differ diff --git a/public/favicon-error-dev.ico b/public/favicon-error-dev.ico new file mode 100644 index 0000000000..89ca25f87a Binary files /dev/null and b/public/favicon-error-dev.ico differ diff --git a/public/favicon-error.ico b/public/favicon-error.ico new file mode 100644 index 0000000000..6e023de88f Binary files /dev/null and b/public/favicon-error.ico differ diff --git a/public/favicon-progress-dev.ico b/public/favicon-progress-dev.ico new file mode 100644 index 0000000000..b5d9d1834e Binary files /dev/null and b/public/favicon-progress-dev.ico differ diff --git a/public/favicon-progress.ico b/public/favicon-progress.ico new file mode 100644 index 0000000000..ca6b90ef6a Binary files /dev/null and b/public/favicon-progress.ico differ diff --git a/src/layout/GlobalProvider/FaviconProvider.tsx b/src/layout/GlobalProvider/FaviconProvider.tsx new file mode 100644 index 0000000000..02d4ce5bd0 --- /dev/null +++ b/src/layout/GlobalProvider/FaviconProvider.tsx @@ -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(null); + +export const useFavicon = () => { + const context = useContext(FaviconContext); + if (!context) { + throw new Error('useFavicon must be used within FaviconProvider'); + } + return context; +}; + +const stateToFileName: Record = { + 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( + '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('default'); + const [isDevMode, setIsDevModeState] = useState(defaultIsDev); + + const setFavicon = useCallback( + (state: FaviconState) => { + setCurrentState(state); + updateFaviconDOM(state, isDevMode); + }, + [isDevMode], + ); + + const setIsDevMode = useCallback( + (isDev: boolean) => { + setIsDevModeState(isDev); + updateFaviconDOM(currentState, isDev); + }, + [currentState], + ); + + return ( + + {children} + + ); +}); + +FaviconProvider.displayName = 'FaviconProvider'; diff --git a/src/layout/GlobalProvider/index.tsx b/src/layout/GlobalProvider/index.tsx index 3b70af823e..2d5a7c7225 100644 --- a/src/layout/GlobalProvider/index.tsx +++ b/src/layout/GlobalProvider/index.tsx @@ -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 ({ > - - - - - {children} - - - - - - + + {/* {process.env.NODE_ENV === 'development' && } */} + + + + + {children} + + + + + + + {ENABLE_BUSINESS_FEATURES ? : null}