From cbbfe02f3d74f4f5f8c2b36f62d2782b103b9a97 Mon Sep 17 00:00:00 2001 From: Chinh Le Date: Fri, 25 Jul 2025 16:59:28 +0700 Subject: [PATCH] feat: sound --- public/sound/button_down.m4a | Bin 0 -> 3348 bytes public/sound/button_up.m4a | Bin 0 -> 2830 bytes public/sound/paper_rubbing.m4a | Bin 0 -> 7002 bytes public/sound/pop.m4a | Bin 0 -> 6893 bytes public/sound/sharp_click.m4a | Bin 0 -> 3101 bytes public/sound/slide.m4a | Bin 0 -> 6940 bytes src/components/layout/Header.tsx | 31 ++++ src/components/layout/TableOfContents.tsx | 15 +- src/components/ui/button.tsx | 67 +++++++- src/contexts/SoundProvider.tsx | 186 ++++++++++++++++++++++ src/contexts/layout.tsx | 16 +- src/hooks/useAudioEffects.ts | 26 +++ src/pages/_app.tsx | 11 +- src/types/audio.ts | 32 ++++ 14 files changed, 370 insertions(+), 14 deletions(-) create mode 100644 public/sound/button_down.m4a create mode 100644 public/sound/button_up.m4a create mode 100644 public/sound/paper_rubbing.m4a create mode 100644 public/sound/pop.m4a create mode 100644 public/sound/sharp_click.m4a create mode 100644 public/sound/slide.m4a create mode 100644 src/contexts/SoundProvider.tsx create mode 100644 src/hooks/useAudioEffects.ts create mode 100644 src/types/audio.ts diff --git a/public/sound/button_down.m4a b/public/sound/button_down.m4a new file mode 100644 index 0000000000000000000000000000000000000000..47ce4559d6e201214500b5152e109bbb838bdf54 GIT binary patch literal 3348 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%0DH?F3I5%Uu z8QHrr?Hb6o9NXc}p6wTNotBODdk^i4nA7B~n%Ly+$IE8j-ID~L;E`gbPor0PN$iC$ zRLvqvkal*F5ty5_{Kf!b7^%E5l%g#j$BN=v!PSg@neQuF!TNlY|Bv@TaoT%TD) zoFEN5d2Bo(`Wk;_>yz|4T>5i;ISDvoL6Iugs8m$LUA&v zDT?FfOkD)Ni_L1#H<_1$-}-GTJr*;Eg}elFq$#2l zV*OssDh^v#JeU9R!@Acew8~WY>uy)ZN-CqzzRo0KPuK^kU;L0%XhvvV{MxJz28Sb5<*oMhW&F+g&RRZ z-DMaBq=q;v*vUEeovazpYde!U=B;7^qB)Wcj(CVd3?D(|)7@>|Ro#ayvk^C!Zl>?i zMtgJc3HcOCwA=E1s4vQs{5Z%abhVQ%`v}nnuwc@RrQxP`w_5P-mP#P9B)?~ulSjHO zn&-F-gf>jY@-Sgoqb*oQmNsp^56(qk@7`&y*S~pKr0N9emu-3F(UEh9CHJo#O&+xn z;m#UNrJ}#~`i%F5YL4>xg$s5?=dQjbo6!gkEOap;WvXl{Q2 z8s>Qo;j+GK&S6^FQsx{oW3#q;N0Om(k%*dthUOP{rLJ>WJ_p)XgYAq6J=SNY`PclC zr81~dr6G7lvj7;$rWK28bKeBBpPzpip`yag1OIX}at){D5WFvzDS{sj?>Jt2BpQAr z;9Ng$$~L#=I;ucRSJ^<;34+GUrT z&a_2)zQ|WM%(9xPJ}d&q)5ng3@}RUF|>{s|Wgj^c@leszXjqdjH%h=z_-h{v8AG^9u0%R~g4f zU>&Vt=ZVHR;(Q>lJX}4UzyjRQ<3ECOqW{t}p?w{3cAVhSAPw-vu>Mts#Q6E!W5BYr zpTFO~CIjbV=HV@lOB5%#`dfA-8i z0PSxFnk+pW|1i#t?Vl?ETs6LS-rl%~{}w^lzm^$SmA@A*jjP~aRk$Fyz<`cjpp&HV MbrH$yBErJ|1#xywjsO4v literal 0 HcmV?d00001 diff --git a/public/sound/sharp_click.m4a b/public/sound/sharp_click.m4a new file mode 100644 index 0000000000000000000000000000000000000000..cf336af073cc33a9905809038be172e4443946fa GIT binary patch literal 3101 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 literal 0 HcmV?d00001 diff --git a/public/sound/slide.m4a b/public/sound/slide.m4a new file mode 100644 index 0000000000000000000000000000000000000000..6e7806b3a74b739188a1c9934a364191b5703625 GIT binary patch literal 6940 zcmeHsRa9I{^Y0$q-GjR`Xn;U)f;$WZ2{!oP8bX2vcMA|)2Fu`XAvjEMNC++=1Pz1) z3ki2P=X`ga?|&ce(|x5!8^nxA%-!_ZLW% zz$yLASd@HpNFxen3PPxWXA-tN!q1WJJS5_x#Wf<0Z>m{%i;A9iJE0=K2cFrV_bcw5 z7E+C^(2KH0nU@fRuHY%-$*hI24Il^J5t%K5m+3p3!KL9Dvw=iGNsgm+0#b4Y+)%7<%Cb$^?~PQw;j7sv2~(9%RoRk0R{3#l_WRqX z1cYK;`fe!_V6f{E(0yU?Uy)Ab=nQTgPZ+;W(wo@$Gn1wp^L2!-ZQGnhn1U&gEV|usi#dBG0qz=#|8WL#K^S zLmhsB=B6w_`}{Glh32CXbZDHGmVk&RA40)+kkAd{ZL8x#tvIl!H##P$qD}&zW8zH#<#0^e886Wg8ihC|s{%W| z4X?&@3Eke-@GHb&&ZvFK3}ozN#0-mc%i(nH@R}jv3l@B~GcKyELD$zv9eZ7`y!6Mg z)Y}bW$Eco_1~lr}>A#)8o2DMX=7SCdRUrq>pU8VWrv5&qXWv!4LiW+BFrsWVhMlQJ2 z-ZI2zM9IR>?2G0JqN2)fOfiNa+TU+k)u4=BUwcc}o6(zUvb5a3)2rArM$cyFXobS0 z(~04!!`lj<8Oc={m|5|6>{f5pm-VU!nThW)oM`Slwo4!E0vo97fFUa{)`mH|QUQlr z`$DfM9D!##h}P_NAFY$4+M;XR@7Gka_pu*C-FKp#)4+`e|zYqT8@>E)vav zBd-Ze8|(d*x@3vp{L5*n4sqv)v!vgDTY5|mW1+2qo>UmUfGO>*GXE}-j+G^zyU|sk z0!q3iwudFR%UrK$C_?P7@H9hnt|E3A#&w$$>A{neTX?J_2ryd_!UPX-hIAFCEi zdFIUSkGkC-Y$wLV1!QQXLu#hXj6X^_mNN_enGKa+barbe$HnVcsMoV4LAE?t?rflR zDi19~W7scv_i9~F+ zQm`&7>*9#$(v~EI7JF%nj}ICjrFmUe5_m4r|8!I44TUkS{#b_!D{kfuxpC<~p;oMv zheQZG4XnGG?Bi`tU6dR0#eIA;S`xGtTmkudEnWzaVf} z&}nJ))~eU!6@DBmR>9F${iJ$(=``z_^YplV7deDgm)GPn)*Pn-vJL~y6Do|7yOGzO{uaEi{floq!5ndA&JKR#O2y|_VjmKo0 zWCuT@jc@b47rt%qimE-R{dH|VQ;=Wy1kK8iTh7)rT~ZGNq5Wj)+P6;A<`c!{7Ef)30w#W$WzO4 z`8-(Bp~s0c%Q3hyefTJHO)o*y7RW#3(NI_Gk0HcToy+P~781lJVhOt*_HfPC)SQWl zBoZqu4z(_(d_AF?&y18rt!ZUp!KEyvu3bs?s3IQJ#2cr)B&G+hW;Ie39KG+eTc=uY z{uW*KW{o=Mw3qAnNIcw~Bt%LfmaAe!KNrmS!DP^e22W9BpF)Gtpt!cJ3A?NI(0A3^ z*AUs8A%56sDk&5ic;i=)&A=rTam3oQ#Ph?$GifK|{Apgm<7aZad&mwnGc07N*3Ns& zGIbVd!=i9ZlHKdRSAia;g)1VbNTtmffU?M6lPJ6!SEy0-O)Tx^^l;D~iMJ0YP>$o7 zw+IYlm!E437>|qUsqjn%Mz$}Ibyj`ixmRqS1Ec3^3K)^`9C>IxcsRPsc_usOn$qC* zplfgz5aVQJh>2z?mgi>kKi%iGMKW_@rVu@wen)f_o$Z>lE*HIc(Co?|5tP06^HFq&?2yp zF&U9yOzYIu-?!;vBE`!+hPLBY%R|?}JE?RY(r{=T6DFL2Xwv+m<#wSm=RC>zQDyc^ z<8*BQrv^SLi&+VfY-GZ@`3v#Ew(TWz1JQ#I;9otkbF)M=4vdxuTXZhTrmkRHsTo9- zLQZaFhCBNZUH-NUMlp5hZFG@NJFx?{?5lyn*n8jQT?KaXxmsg*97H2rzN&K0$ZS|J zd`{Ch#~@hq80ulWuIQ4}d9mWS(*V8F8kd@*R5=sT$1uVvW=otc`~85h&w?53g62$o zV)is`0e(5f_4?W3kHa7|Unh7F2VFn%%^McbOOlpVoHZn z1=BCA8o1%gditNXGAi*+BRGtvMJKC1tLcV*4a{KZ&k&$tv}27_7`iG@AsqcYmxsH4469n zus9o>+Ct^Sgm^Q$yW;v>=?DGz1pJGha+HxO_wDhH^PjD@@%}IQCYwOi<$Q-D7JyUj z%78VDYlbh0_+HJIJ=Swu8NND0M9_ed#l*faaQa`s_ic|6JFXX$kzIhln|7H6e4#3~pGDmB*>wKr7c91Re8Q-TES zIV-$zlpg~ejXylQF6ig2LLb$z;qc<_UHb;*mJ4!VzTPizpAVquKgo!>s-0`Cxaux#NeC&QlqM?|6UgGlj{YwtR*gCuPv2{F?O@P)Q0c64E9Sd!p3RqF;IU zxVZtXe+yvHfijOeiE5N+eXb4et6`~T=ACaBx-p*LKZG*+HPs!27$)C5A!VGOARNl7 zzh2WBDXY;LZBc775RhRt-TQtuF>*d0E7&5VGtA~x{bf}c8rXlxBM@93Q*?m8lcj*! z2+ca_K3-DtmFKJw1MOYlNs@U;o9~xxySYl>oN*RN8yEWp#zEq-z$cI#W%IPo)GRIs z(+`h7AgUuOKY3j@*~QyBoj$hozg3T?B1D9ce2AqYYSNx!-DD1%*I*&X{ZT)iKtal{ zO3QbzN}u!jB2uvr^(A-WG@PE^y3V`q)phoEp!Y=CA(|7x7$T*Zzra+jQXHwLXa$FV ze0ZtB-Eol$6G`1yDBi>Od(OZqNrug8Axdk3-99KJ`hfCkuZXGkR7}~pN z`}UT}p;R=bq;Ysnc#YOG5(h+xrqy|n)ci!knY+$?=NS8>hnV>2S(ChDfS>80``MYM zOLzU5UCr|0rPGDKN#gD^mlCT#ET52Bu=O413FkydgPe zJiL-Qu>%`N?PthS`IiTcSm+16JtujluZkY_K6dCgg(P7FvbRahWj*p*5bdetrD2>8 z*MX>tuQs2zqNO^H0yLkwE`p1u+?KwnBnEDmIxNRp(4#(j{Djjnl<_odqorOS4`te8 zl9)<K7+|L3D7Wd!e zymY&r{@Bv>oSeTLi6v{bJosm$QtpY+!h>VF@Tz5*kAh|2G>PGU+-WryiSD1sl_Cx} zsChhna=^2~fbb*X-_y;!w5uK4PE{x>gUJL{s zW>nt?tU^(nLPifFDVhU!iA!Vmo`-Fp6Ar!J2eoDmbPbi~DVyDRKU>=?Mt`%lUvRR) zEW~`Q1cxX*x#U8=-lRYL@VhoeW5_%?OD4ww9?+>(vZd(cT#(!J<~Ua)7BT5b4sO3ya+cLEw8(heM9`a5rwqKT zFpP<&&o>SMG9RR8T#*F)h3CoB%oBk}Q@*rcan$03aA8>1eqXami)?J0Or8Y41HYF2 zvRHfWcb#eX5IV8}INPFE(>w;ZWecp{53(kKf}dyV9i-H}wfu*Uh%fA^x_! zk^~OA9|gK(R5&#vZ0TcdW)=8zVkwBc{pR-gOb#oz2du|*Mh9DW)2fn{HK3FL*uIYD zu0iWV>r2tSxaXVr1Vh=DwaD6L2ip}!Mz33zJ!IC=(>gn}FCMush!jIBF^OlV^h?Gg zqNA^F3klB;(Ke*=6=Bkv3OR+>tX2`XqzHK9f7vs?!c=m}Q7Sg0wtXxMISQELBqrLw zPC+KE=3gYxJe=|PB^L?Kfo>%v1Pmg>?wddHIJJ1E)!92keE(z*(~@fBMu-nk5K*ao zU<;uD2O;ED%$&Yvai>J_NMlKOSesJR8$f_LiHig*;gi+PiD!021UqGGqKUl-LkhU6 zpDPW-yYT)`wc zxh3`ze2Zw6FKDvi7%ogqY2{iQ=x-xk8F}O+Z5b}cs&p0(8EkYiw&S)4ZCKp0!|T2jrG!792~C4YD34EJO0vuh=iB|hnKizhJ`Mi(N9%Y#m!p> zvNYNdsX=@j!f6xv0w*QfF-o!OwQBk2#)e!5b?zRuGV8a7&|&3+?Bp~ygSQ5tbYAcI zR)6>#CvJ}k{bzfE^AwOjyh2~1`X@KYjb8=xR8pkQm{iVizI^OTS{I|a`Zc4-bFTKYcftzmq+R|BKMS z0sx2w0HNA{Y5%)}GW?ej0ObGe`+v%4sPhCXUvC>%l*z)^^{<@h|8n~$8i?iJy1$hF zBhUXOFFVxU*9R4Y9_r@f`%f%X1Q0Xo_~M_MK_6iD&Ne6;GtB;fj{S`?07x|%52CC9 zAmC*0=KYt1YJ5EW-2WrraYIz!Ud_hc-VJ)^0}6vVyE~u+5+B%q1!cGYTTgHA4ZXA9 z(cO>;{JhxfAY6MUg1f$Np}> z0D5R>C@abh1H~!iDeqKh65DscL}}2-NU%^0gjxrH*~j;F%(iz$Uq?+1t%0NP|!yK zf`abf04RKX?L7Voc9(}A1v(!`Py4%$w};!^wA|GUy1hlU^ggzZo+$nw%fBnL^|SZ2 zL75)Ipnvf@H void; @@ -19,7 +21,19 @@ interface HeaderProps { const Header: React.FC = () => { const { isMacOS, readingMode, toggleReadingMode, toggleIsOpenSidebar } = useLayoutContext(); + const { isSoundEnabled, toggleSound, playButtonUp, playButtonDown } = + useAudioEffects(); const modifier = isMacOS ? '⌘' : 'Ctrl'; + + const handleSoundToggle = () => { + if (isSoundEnabled) { + playButtonDown(); // Play before disabling + setTimeout(() => toggleSound(), 100); + } else { + toggleSound(); + setTimeout(() => playButtonUp(), 100); // Play after enabling + } + }; return (
@@ -63,6 +77,23 @@ const Header: React.FC = () => {
{/* Command palette */} + + {/* Sound toggle */} + + {/* Reading mode toggle */} diff --git a/src/components/layout/TableOfContents.tsx b/src/components/layout/TableOfContents.tsx index eae5a9c05b5a..b68d12efc873 100644 --- a/src/components/layout/TableOfContents.tsx +++ b/src/components/layout/TableOfContents.tsx @@ -5,6 +5,7 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import HeadingNavigator from './HeadingNavigator'; import { flattenTocItems } from '@/lib/utils'; +import { useAudioEffects } from '@/hooks/useAudioEffects'; interface TableOfContentsProps { items?: ITocItem[]; @@ -21,6 +22,7 @@ const TableOfContents: React.FC = ({ items }) => { const [activeId, setActiveId] = useState(''); const shouldBlockHeadingObserver = useRef(false); const timeoutRef = useRef(null); // Still needed for blocking observer + const { playSharpClick, isSoundEnabled } = useAudioEffects(); const scrollToId = useCallback((id: string) => { const element = document.getElementById(id); @@ -53,13 +55,17 @@ const TableOfContents: React.FC = ({ items }) => { width: `${getIndicatorWidth(item.depth)}px`, borderRadius: '2px', }} - className={cn('bg-border mr-2.5 flex h-0.5 text-transparent', { - 'bg-border-dark dark:bg-border-light': item.id === activeId, - })} + className={cn( + 'bg-border hover:bg-border-dark/70 dark:hover:bg-border-light/70 mr-2.5 flex h-0.5 text-transparent transition-all', + { + 'bg-border-dark dark:bg-border-light': item.id === activeId, + }, + )} onClick={e => { e.preventDefault(); scrollToId(item.id); }} + onMouseEnter={() => isSoundEnabled && playSharpClick()} > {item.value} @@ -80,7 +86,7 @@ const TableOfContents: React.FC = ({ items }) => { style={{ marginLeft: `${depth * 16}px` }} href={`#${item.id}`} className={cn( - 'flex cursor-pointer items-center gap-1 rounded px-2 py-1.25 text-left text-xs leading-normal font-medium', + 'flex cursor-pointer items-center gap-1 rounded px-2 py-1.25 text-left text-xs leading-normal font-medium transition-all', getHeadingLevelClass(item.depth), 'hover:bg-background-secondary-light hover:dark:bg-background-secondary', { @@ -93,6 +99,7 @@ const TableOfContents: React.FC = ({ items }) => { e.preventDefault(); scrollToId(item.id); }} + onMouseEnter={() => isSoundEnabled && playSharpClick()} > {item.value} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index d29f86ebdfdd..5d36b2b94e6b 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -3,6 +3,7 @@ import { Slot } from '@radix-ui/react-slot'; import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '@/lib/utils'; +import { useAudioEffects } from '@/hooks/useAudioEffects'; const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap font-sans rounded-lg text-sm leading-6 font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer", @@ -35,22 +36,80 @@ const buttonVariants = cva( }, ); +interface ButtonProps + extends React.ComponentProps<'button'>, + VariantProps { + asChild?: boolean; + enableSounds?: boolean; + soundOnHover?: 'slide' | 'none'; + soundOnClick?: 'pop' | 'sharp-click' | 'button-toggle' | 'none'; + isToggled?: boolean; +} + function Button({ className, variant, size, asChild = false, + enableSounds = true, + soundOnHover = 'none', + soundOnClick = 'pop', + isToggled, + onClick, + onMouseEnter, ...props -}: React.ComponentProps<'button'> & - VariantProps & { - asChild?: boolean; - }) { +}: ButtonProps) { const Comp = asChild ? Slot : 'button'; + const { + playSlide, + playPop, + playSharpClick, + playButtonUp, + playButtonDown, + isSoundEnabled, + } = useAudioEffects(); + + const handleClick = (e: React.MouseEvent) => { + if (enableSounds && isSoundEnabled) { + if (soundOnClick === 'button-toggle') { + // For toggle buttons, play different sounds based on state + if (isToggled !== undefined) { + if (isToggled) { + playButtonDown(); + } else { + playButtonUp(); + } + } else { + playPop(); + } + } else if (soundOnClick === 'pop') { + playPop(); + } else if (soundOnClick === 'sharp-click') { + playSharpClick(); + } + } + + if (onClick) { + onClick(e); + } + }; + + const handleMouseEnter = (e: React.MouseEvent) => { + if (enableSounds && isSoundEnabled && soundOnHover === 'slide') { + playSlide(); + } + + if (onMouseEnter) { + onMouseEnter(e); + } + }; return ( ); diff --git a/src/contexts/SoundProvider.tsx b/src/contexts/SoundProvider.tsx new file mode 100644 index 000000000000..0d6f37b2cd49 --- /dev/null +++ b/src/contexts/SoundProvider.tsx @@ -0,0 +1,186 @@ +'use client'; + +import React, { + createContext, + useContext, + useState, + useEffect, + useCallback, +} from 'react'; + +interface SoundContextType { + isSoundEnabled: boolean; + setIsSoundEnabled: (enabled: boolean) => void; + playPaperRubbing: () => void; + playSharpClick: () => void; + playSlide: () => void; + playPop: () => void; + playButtonUp: () => void; + playButtonDown: () => void; + toggleSound: () => void; +} + +const SoundContext = createContext(undefined); + +interface SoundProviderProps { + children: React.ReactNode; +} + +export const SoundProvider: React.FC = ({ children }) => { + const [isSoundEnabled, setIsSoundEnabled] = useState(true); + const [audioPool, setAudioPool] = useState< + Record + >({}); + const [isInitialized, setIsInitialized] = useState(false); + + // Initialize audio pool with multiple instances for overlapping sounds + const initializeAudio = useCallback(() => { + if (typeof window === 'undefined') return; + + const sounds = { + paperRubbing: '/sound/paper_rubbing.m4a', + sharpClick: '/sound/sharp_click.m4a', + slide: '/sound/slide.m4a', + pop: '/sound/pop.m4a', + buttonUp: '/sound/button_up.m4a', + buttonDown: '/sound/button_down.m4a', + }; + + const pool: Record = {}; + + Object.entries(sounds).forEach(([key, src]) => { + pool[key] = []; + // Create 3 instances of each sound for overlapping playback + for (let i = 0; i < 3; i++) { + const audio = new Audio(src); + audio.volume = 0.4; + audio.preload = 'auto'; + + // Handle loading errors gracefully + audio.addEventListener('error', () => { + console.warn(`Failed to load sound: ${src}`); + }); + + pool[key].push(audio); + } + }); + + setAudioPool(pool); + setIsInitialized(true); + }, []); + + // Load sound preference from localStorage + useEffect(() => { + if (typeof window !== 'undefined') { + const savedPreference = localStorage.getItem('soundEnabled'); + if (savedPreference !== null) { + setIsSoundEnabled(JSON.parse(savedPreference)); + } + + // Check for reduced motion preference + const prefersReducedMotion = window.matchMedia( + '(prefers-reduced-motion: reduce)', + ).matches; + if (prefersReducedMotion && savedPreference === null) { + setIsSoundEnabled(false); + } + + initializeAudio(); + } + }, [initializeAudio]); + + // Save sound preference to localStorage + useEffect(() => { + if (typeof window !== 'undefined') { + localStorage.setItem('soundEnabled', JSON.stringify(isSoundEnabled)); + } + }, [isSoundEnabled]); + + // Generic sound player with pooling + const playSound = useCallback( + (soundKey: string) => { + if (!isSoundEnabled || !isInitialized || !audioPool[soundKey]) return; + + try { + // Find an available audio instance (not currently playing) + const availableAudio = audioPool[soundKey].find(audio => audio.paused); + const audioToPlay = availableAudio || audioPool[soundKey][0]; + + if (audioToPlay) { + audioToPlay.currentTime = 0; // Reset to start + audioToPlay.play().catch(() => { + // Silently handle autoplay restrictions + }); + } + } catch { + // Silently handle any playback errors + } + }, + [isSoundEnabled, isInitialized, audioPool], + ); + + // Debounced sound players to prevent overlapping similar sounds + const debouncedPlayers = useCallback(() => { + const debounceMap = new Map(); + + return { + playPaperRubbing: () => { + const key = 'paperRubbing'; + if (debounceMap.has(key)) { + clearTimeout(debounceMap.get(key)!); + } + debounceMap.set( + key, + setTimeout(() => { + playSound(key); + debounceMap.delete(key); + }, 50), + ); + }, + playSharpClick: () => playSound('sharpClick'), + playSlide: () => { + const key = 'slide'; + if (debounceMap.has(key)) { + clearTimeout(debounceMap.get(key)!); + } + debounceMap.set( + key, + setTimeout(() => { + playSound(key); + debounceMap.delete(key); + }, 100), + ); + }, + playPop: () => playSound('pop'), + playButtonUp: () => playSound('buttonUp'), + playButtonDown: () => playSound('buttonDown'), + }; + }, [playSound]); + + const soundPlayers = debouncedPlayers(); + + const toggleSound = useCallback(() => { + setIsSoundEnabled(prev => !prev); + }, []); + + const contextValue: SoundContextType = { + isSoundEnabled, + setIsSoundEnabled, + toggleSound, + ...soundPlayers, + }; + + return ( + + {children} + + ); +}; + +export const useSoundContext = (): SoundContextType => { + const context = useContext(SoundContext); + if (context === undefined) { + throw new Error('useSoundContext must be used within a SoundProvider'); + } + return context; +}; diff --git a/src/contexts/layout.tsx b/src/contexts/layout.tsx index 74f00b2f7b54..7b058c30f8d6 100644 --- a/src/contexts/layout.tsx +++ b/src/contexts/layout.tsx @@ -1,5 +1,6 @@ import KeyboardShortcutDialog from '@/components/layout/KeyboardShortcutDialog'; import ShareDialog from '@/components/ShareDialog'; +import { useAudioEffects } from '@/hooks/useAudioEffects'; import { useIsMounted } from '@/hooks/useIsMounted'; import { ComponentType, @@ -49,6 +50,7 @@ export const LayoutProvider = (props: PropsWithChildren) => { const [readingMode, setReadingModeInternal] = useState(false); const isMounted = useIsMounted(); const [isMacOS, setIsMacOS] = useState(true); + const { isSoundEnabled, playButtonUp, playButtonDown } = useAudioEffects(); const [isShortcutDialogOpen, setIsShortcutDialogOpen] = useState(false); const [isShareDialogOpen, setIsShareDialogOpen] = useState(false); @@ -96,8 +98,18 @@ export const LayoutProvider = (props: PropsWithChildren) => { }, []); const toggleReadingMode = useCallback(() => { - setReadingMode(prev => !prev); - }, [setReadingMode]); + setReadingMode(prev => { + if (isSoundEnabled) { + if (prev) { + playButtonDown(); + } else { + playButtonUp(); + } + } + + return !prev; + }); + }, [setReadingMode, isSoundEnabled, playButtonUp, playButtonDown]); useEffect(() => { if (!isMounted()) return; diff --git a/src/hooks/useAudioEffects.ts b/src/hooks/useAudioEffects.ts new file mode 100644 index 000000000000..ca92c762224f --- /dev/null +++ b/src/hooks/useAudioEffects.ts @@ -0,0 +1,26 @@ +import { useSoundContext } from '@/contexts/SoundProvider'; + +/** + * Custom hook for managing audio effects throughout the application. + * Provides sound players that respect the global sound enabled state. + * + * @returns Object containing all sound player functions and sound state + */ +export const useAudioEffects = () => { + const context = useSoundContext(); + + return { + // Sound state + isSoundEnabled: context.isSoundEnabled, + toggleSound: context.toggleSound, + setIsSoundEnabled: context.setIsSoundEnabled, + + // Sound players for different UI interactions + playPaperRubbing: context.playPaperRubbing, // TOC hover - organic, subtle + playSharpClick: context.playSharpClick, // TOC click / precise actions + playSlide: context.playSlide, // Button hover / smooth transitions + playPop: context.playPop, // Alternative satisfying click + playButtonUp: context.playButtonUp, // Toggle ON state + playButtonDown: context.playButtonDown, // Toggle OFF state + }; +}; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 3139dd489042..aa0fbee4df6c 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -4,6 +4,7 @@ import '@/styles/globals.css'; import type { AppProps } from 'next/app'; import { Toaster } from '@/components/ui/sonner'; import { Web3Provider } from '@/contexts/Web3Provider'; +import { SoundProvider } from '@/contexts/SoundProvider'; import { useEffect } from 'react'; import { useRouter } from 'next/router'; @@ -38,10 +39,12 @@ export default function App({ Component, pageProps }: AppProps) { return ( - - - - + + + + + + ); } diff --git a/src/types/audio.ts b/src/types/audio.ts new file mode 100644 index 000000000000..e76563efc74c --- /dev/null +++ b/src/types/audio.ts @@ -0,0 +1,32 @@ +export type SoundEffectType = + | 'paperRubbing' + | 'sharpClick' + | 'slide' + | 'pop' + | 'buttonUp' + | 'buttonDown'; + +export type ButtonSoundType = 'pop' | 'sharp-click' | 'button-toggle' | 'none'; + +export type HoverSoundType = 'slide' | 'none'; + +export interface AudioPool { + [key: string]: HTMLAudioElement[]; +} + +export interface SoundPreferences { + enabled: boolean; + volume: number; +} + +export interface SoundContextType { + isSoundEnabled: boolean; + setIsSoundEnabled: (enabled: boolean) => void; + playPaperRubbing: () => void; + playSharpClick: () => void; + playSlide: () => void; + playPop: () => void; + playButtonUp: () => void; + playButtonDown: () => void; + toggleSound: () => void; +}