From d95e069d992e1d4bf7c9390ed716de820d1fda21 Mon Sep 17 00:00:00 2001 From: Nikolai Berezovskii Date: Sun, 30 Aug 2026 20:23:32 +0300 Subject: [PATCH 1/2] feat: add optional clipboard capture sounds --- Copy/App/AppCoordinator.swift | 5 ++ Copy/Resources/Sounds/SOUND_LICENSES.md | 34 +++++++++++++ Copy/Resources/Sounds/copy-bubble-pop.wav | Bin 0 -> 19838 bytes Copy/Resources/Sounds/copy-click-tone.wav | Bin 0 -> 17758 bytes Copy/Settings/GeneralSettings.swift | 24 ++++++++- Copy/Settings/SettingsStore.swift | 38 +++++++++++++++ Copy/Support/CopySoundPlayer.swift | 57 ++++++++++++++++++++++ 7 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 Copy/Resources/Sounds/SOUND_LICENSES.md create mode 100644 Copy/Resources/Sounds/copy-bubble-pop.wav create mode 100644 Copy/Resources/Sounds/copy-click-tone.wav create mode 100644 Copy/Support/CopySoundPlayer.swift diff --git a/Copy/App/AppCoordinator.swift b/Copy/App/AppCoordinator.swift index 56f3386..0c1a071 100644 --- a/Copy/App/AppCoordinator.swift +++ b/Copy/App/AppCoordinator.swift @@ -356,6 +356,10 @@ final class AppCoordinator { } let settings = SettingsStore() self.settings = settings + // Start listening before the shelf can perform an explicit Cmd+C. That path + // marks its pasteboard write as internal, so the clipboard monitor correctly + // ignores it and cannot be the source of sound feedback. + _ = CopySoundPlayer.shared let linkFetcher = LinkMetadataFetcher(store: store) let ocrController = OCRController(store: store) let reporter = saveErrors @@ -381,6 +385,7 @@ final class AppCoordinator { // Already on the main queue; hop into main-actor isolation for // the one-time activation nudges (see the methods). MainActor.assumeIsolated { + CopySoundPlayer.shared.play(settings.copySound) AppCoordinator.showFirstCopyCoachIfNeeded() if !pasteStackModel.isActive { AppCoordinator.notePasteStackOpportunity() diff --git a/Copy/Resources/Sounds/SOUND_LICENSES.md b/Copy/Resources/Sounds/SOUND_LICENSES.md new file mode 100644 index 0000000..7871071 --- /dev/null +++ b/Copy/Resources/Sounds/SOUND_LICENSES.md @@ -0,0 +1,34 @@ +# Copy sound assets + +The two WAV files in this directory are trimmed and loudness-matched derivatives +prepared for Copy. Each output is stereo 44.1 kHz PCM with an overall RMS level of +approximately -25.5 dBFS. + +## Bubble pop + +- File: `copy-bubble-pop.wav` +- Author: Universfield +- Source: "Bubble Pop 04" on Pixabay +- Source URL: https://pixabay.com/sound-effects/people-bubble-pop-04-323580/ +- License: Pixabay Content License +- Original SHA-256: `9cffab062f297d47bf9a8b8807b935b9cdb887562e6231fb7cf78763422fcc44` + +## Click tone + +- File: `copy-click-tone.wav` +- Source: "Cool interface click tone" on Mixkit +- Source URL: https://mixkit.co/free-sound-effects/interface/ +- License: Mixkit Sound Effects Free License +- Original SHA-256: `248312c2ef619427ef7024126a846e4210fe37e39038cfd32c73b1fb854a8086` + +License references: + +- Pixabay: https://pixabay.com/service/license-summary/ +- Mixkit: https://mixkit.co/license/ + +## Processing + +Each source was trimmed to its short audible transient, converted to stereo +44.1 kHz 16-bit PCM, and gain-adjusted to approximately -25.5 dBFS RMS. The +bundled WAV files are derivatives intended only for use as Copy's UI feedback; +they are not distributed as standalone stock audio. diff --git a/Copy/Resources/Sounds/copy-bubble-pop.wav b/Copy/Resources/Sounds/copy-bubble-pop.wav new file mode 100644 index 0000000000000000000000000000000000000000..e0da1988613d117b0b7d1f6a7fb178f6b9dbbd03 GIT binary patch literal 19838 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 literal 0 HcmV?d00001 diff --git a/Copy/Resources/Sounds/copy-click-tone.wav b/Copy/Resources/Sounds/copy-click-tone.wav new file mode 100644 index 0000000000000000000000000000000000000000..82381a7678df53c320fc3fa5a3cf2ff09bae5430 GIT binary patch literal 17758 zcmb7q2Y?hs*KJjYNt<_B!U9W9N(K>B1WAe@d?blVP(egQlxzS*FcL&SM1o2X6a-N~ z0YyNRC{c1wOWHg;p+ntwYWtgic18d9-V8O}(_MA%x#yhu5rw{CwTVyDHo3kxpJbCln{Ab9(ra2jDrvP3AI!Xz zJ^Ff=oAa)p%h+-2-CL#er)3W{mgLV4#roUjJ&=EYL66`IYD?v(^Aq=MVN+QoGm|CC8*rNl&kkQmad?oegF+ zsL*8ceTj|I8!f1wd{5hwgUkDT4U&G@)NWR#LXA@8cBLmLTul1hdXlfb^=;0$^IfjQoospf^s#Zr>?1!P8hm)(pXU!9 z{WEy1%h8b+JDuB|Gw}8XHo)i+cg#JzBqgse|7y8SRjKNOwa3+dzrpicjQ~4PGh`{EX6>c{?U+v=ee;qiX9<`3D$N66;&i;7n`qk$y zmb|kg<9yz^?49O!;cf20`h*grl0HoTploiH+)A%k9awdBrFB&*r9WD!AZ=gysU<%w zy)1ru;t(~gmkJIxrrt5L_gw38BlXI6SKhhY?Nas2Z7w#tJm=!Pt1YkWxK-}vtN#4l z-PFa(^|o|rNj>9-rA{ajDBZiv=yEN}UrwJ}VODy_3f;ZMANMyQ@R^Vc#p> zJIPCuDx_^JJv4oA`Jd7+rTfr=Dj(8 zW_)`y<=U2OXRg+`8obu->eh@+*CyXueB=J?QJEtG{rx@Y2TSu*(ND*}7&|0IO@64P zt7O&EJ4<&f^FUgOGCZwo$qA)?PJA!9qxVG2QvR+cjQRG4g4cqra?|q4W)8}nbaTng zDH(5N)V%S1#^T#OZkai!v$6uc{gbUy#uxlB9dmu5_ltco=A)$5iOo_cml#^|k&*}2pKO*k=#F~kXlGY}5OsbtcKXGQ#r|}CDa(&ff zPwOwZ>oA|f%_CMpphGA(f4+ZV?)u#Q*|V}wWR1@1p8b2)%G|9v^9ySFI~o&0mqolC zQeNk)Twm)oeII)V#%XZ}6XquDOx&M%F5#KPK5^sYL*6MdeO=u1i_%DARD&@y%M1n% z2504W_fO6($o1rgax!yU=B~(pH}81ChyLNACczcvbYnhE5x3P{$`$u-u0y_%cWi9= zSQghLZeQ&1I1zIwcCxQ`Oq|E>`BGb}uV9z>A6BfGAO6bd9Lx{yF7Ov@_s{b;^Iypy z>-YJO7Bnk(A~ZbsmN~$f<`>neTbd*V$Jy=DwJ{zLGJIdT)5M zT<6`7Xnpl1e3ep`a_LK}h27X#ZzPAthdv9O3zQF(3oHt>4x9Z#%R*>l1(+;h^i*4^IozN^0bx;9<^MH!|xW;txJJxB~O zQ>~-nw#K;-h5Ch>hVBn#1gC_`hlYmpLY6Vqs9-;Dy+PlK4Lpa9{4RdQ8x z-|L?1-sm3SUhIC}bT2(`x0@-})5ct*Pk3B7Cv+?HKv;yz z87srjn12`-toy97;*@<4TSh@Xg>P27tBv#vS~XWtALE+jdcgIj>tkJWeWBIRhpIX1 zbv{(7%vP|+#R0L=E`xs!pT$bylEE zRw`+yRYSX~y{pgHx9fNG_x1hy5v{hqL@lM2QC2FWn3rD_Lus;o&F)~mZ#`xXF=rZc zj5d(`hSAn&W}Y&7S|`nQb|t$a%@y-l1s0=x!(UV@sdKe~T0gy=zFgm+57$@eyS01u z<7yXel~P-Mg74+~s0C{-z7l(^YWC;mCG(PDLh@weTVteA!Q5qBF~^x9YpT^toVRb# z3-l(}`Ay|*#iRYM9?&*u&Gk-tcl~WWR_~$r)qc|2shiX*{28S;yTFc%uJoFH-tK9A zVvRIsn5T`uj3ve-e12+7GOL@@t3sy?m$a|=X>Hw{Yma2R7r}bC# z`ug+w94$v%3NA(~1xf|Jh_9ontfg2cZdni52Tiw?Xx22h8S9MG#!{o3X&dR*_htio zt2I%$L;+2tSAqG6GDgW#*Q!5g3$!%7rd}1O4{7D}irPf2jQWnchqqR`u!HQRc!DO{ z7w!Jm5^I{d6w*S*VPlQqGxr&5%-%9~na zy`%2aTkE5>{n|7&p!QUbDvZCu=Ti)8B<6`MV7_Q3TQ$rF%p=Bc#+SxV#>{X@b8+~v zIU{`5nqXEF!`K}9N>AiNJ>!%so;B(-o;g}KcLn`~tBvmV==wR&bK2{!;p!1yPnl$I zXM>Ek)IW6EUJ+bmH3=>=TNQ9~Sl-XZr@0pm<{xTS33aiySyOBm&k*_Q)9i@8k5Bca zsPkhhYONB_Ydhlwt>b`^m zZFO8%ZL6=hTFaHL^ko~^3q}h%RdB+to4?R{H+O}Zn&URr>^(+KR<7}G&g*77|1;LN z;g9W>lrQ$H&$Fl9NBIn2X?0;-MXi7039WTPNrksnS`_S@R48aBc803+Qg$V+6W{4Rt8I%}=+22h?CqHNd(47_N8_H1 zTNBsY_ik)ox8M7y_L^%vJEH70Cs0;kl65+7LAYzqkAXGWt^AX6{>n3R&*mM<|BruU zV0)meF+1FxhFY&{1F5lhm9i+dn(M9jUf!n@zKvO%@MCQIgyFFZ;u^&C^NsiJ)t_}Q zVmq~;&DZ#$;12tcKh@}$7atsuQ?{UG_Cx-e+132t<^ht9{>S8L{o$aq-W3 zpN;?0M{#?7KgI^VkNd`Ywzy`x-d1{Roy1|j%&^6e!4&KDf-&KJ`R@jQ$(tBho7W+b zmA^SKEU+TDKAaT3-%d6^X3Okl>bq>btCyPM{a&vV^Q^m5>_|_|*aFXPU)b}3=NZpc zJ;_~4RrQAKRi%o(m2{)6-67cA99Hmoc%6SxsAR$HV7EZ;U}ES>@E)T{_&ck*xq|xJ ze<`2QaeWhS;`vrxaT;16>1A|GF`c+Rm;4%nfOEsHr_5 zIMDnekQIJ0kQ%-axE`t;dMsSuNHPAj?ltGohgNl^q3Eq8uv)G>zR{`66)rqCUk6##5#fQfC_HLt$-QE1tYH1y@X4)xsHJV`m!A!fU@|ozV_M>#IDO;!| z^Ap-0{)^UDX{x=gJffacdMh){$-!a%<2j+gTLmwBE@}^#{k+5n>HA9mocKq=E;UKd zrfVW2G(B+o#?v>ioSAhY?tJ}AdAAep#0E+ST85`vwRxtVkhm%Ny|mJ084#eU(_yiN6FrH)Z8RK@>x-rUf2e?PX!$l*QgvTUe$U%jf7^BwRk^zyYIM;6$ib?gReKw|~#>k$Ed;df;a8AkC%+v;msx`PTDt!iD(jsWVgjCAue% zi8~w9!gW!5L|IPt?QP+&{4sesnG^0jly&6J-2BbCkA z<{Rvb^HueIccZ`YQuWijbz~7)$ zeX=smRZCmxTCP=gb=O|k@|1SG3wzQoYqv7qF@6cP4EG3C2=58`j2T8_>w>+A_VH2b z9c`X#pX&|JK6fi`j{CZ2oh#LSOTDDdXQkO`dy{3GO^lz7g`uuSl~5D2e7L*)l(~~? zh!y+?c3#<{%u{P>bJZWUK}xJvmvvCi*>zY|tFtI$-mz91<*dqPP3x4^+J2d8Q)#6w zZ>}Y%f9eCZUtB5rGS?}sl|BWL(2sX#KJp1}SGHa;cN)`;LB`icP4l$5-8yK`6&}`& zRaI{AJ8GI*MxUs5({q$JwIBE?Hx>P?ruN6?C2N*h!ERxFXTM;#7nejbjbVo=m(OIe z>WlmnwY2iFT2V<-$M7o3XjZ`9q(8_@m&JG*EuLX72_Ii89^ut#5Z_84@J?(N|Auwv zo7pZliG4*0On|wktgYf@vkvytWXD@AMLGMpXe*Y{2%61~vT1yx(nU$ub}JtJh|)lR zMOmV4=3~`gSfVnYPO(Yi7X5(VA3E5HqK)0go^Kzw9u^r^It{lISSfLly)DM_#o`aX zQXJrC#7zDWmEnKT2ka#l&s6>cy~N+4s~8_CKTucIqyuUzwoM(*N~ptFHgC@6vTO7x zy+ozNZ4t62h@ja_v@{vctsLW4VUMudYzsR{UHMYV=aXoH z(t-X^EU`-oim^&by2D?l`}i&D#hS4})RYYtHchj4(rxQk`p!yXXRZ0{DcjAHMQwgr zROFi}j(^AktUce&PVr}1ZDkGJudEXuWrw|qW53Dk*@=7#I#8UL$kvNS>}9$}z1cz< z$$Qe#%{! zH{@4p(SzznQC{sQzEY|QRVgd_^T)-0wobHVZD=dy(7iODEfH0CWAPTOS<5fktNAJW zb$-;YisO2wi6-ntv5S(Bw`ZsYea#xt5Z;nJN?&Bw1S&wbrmO2IPuWG!EBoQ~-SjE@ znU>Q}bV&S8Rm5%j+^&kg^gL@}uVj<$)66SEY>TkiOLUpVu$62Y>%y+H>(rEwr!l-N z&Eprv4F0DW!mo*jyc{t;oKCR)w2IYX&#@_t=vOw4&aebJ!oC&Xpf9&%Ekrd@SFA8w zh_>cLv0DU%Tl<0%-Tm1pS2B;&7xVp^!RM+~l?{}x)G@E}@Zu(bJ@F!%GYBoQZm_a zWgGCm$DUxh;-IK$W!N@;9m}xZuq*Nz_D!vxh;@ymM*458llCdEruun*R!Lc4r6}>n zD1Ii~o*gh!X^PoVd~IE{8`BqdJ!Pf6TWujeQoGYNX5l&6MIERNJ0dJTL5x#Jh)L>MF;!VgEBSNmHhzs6PhI$Udj`M9n!wv5-tV*1 zSv;PWIjs_F*}dW$<-C1Fy<=~{T1#n8ZImQdm+xoYXax3aH+f6Tr?jo--*u3kK!Z6OP?rH=n=)sI`i2qNJ;!SqP3M6#p6X&9*Y|AyyyfRztKDFVZ`(~ zk-@hLML8qRp!&As?^8Ye+Mg+2V*&dV8)hr~K0A+fvwvm<_Dd{{;@I=(JFoG6)J$oC zr=N~=NEu5*l^^L{?nRI7%RWPe%n-}j5HW)_6MfJx6IoT-OlRqNdWAJcX4R$|yaQE4 z2i(BUl8#?Hy3!K1Q(R;Hg^sFaBX15eA*QecREaHMTj_EB0v+MaC{3w>9eYih%-hiK zY&8C6@DsI0SN}>p$2uUAI*6~>Qt>vj;4$_m#j@cnhk`6XPjH3(2Rj!xjvwRi7kKJm zA5bgQu1@Ttc$nQ1Zmju%KB2)_bw5pF_aJ|ok%#x9Og4!Yvn|vJ>$cGZ)|t+t%ar3+ zM1Af-XMYMakI-EF{?P%u2bDEMhwse?Qwx3%<*|5ri>MFu=|eV;o?^?X8tl4FUbdDx zv*9!YT>ijBMeL|{VFyu$CDLKfT8v76V_!m?H3>eU`6IEam z@IK7WGcUefp!ZN4o3q9E4f8G9h?+W!hSOvkMeoyVw36PWqx1%GHW3JBpmwZ7yk4cc zES<%&p6oJe(01C0tEbs_u=+RhV-ItJrLu*r9qY(mMkKz6?(!jf0SrDwU06jb$xvLtYQo92K__)7iby zQ;r=Z6*+Q&4gvWE`Up0R0uO`8&H9qY`csfzrOWgw?S;mTR0>G?vDGvW7H?(e=>YSw zz39~&P{Th$&F{xPVddFuFh2o|cfuaCG#duFLuobSOa(t9fqEpZqqpe*em}hdy$sbh ziB-nB=7_DwA?-2N8Rvt6a6Bu8-r!+xp+Ahl7|7h%L!`jE3hWAg=l%vcJdV3^?hvyO;!^rHi%*9$Ef`2dojdV zFV+^`YX>j3WUsKM>?u|cS8K6ytU7qB$}Yja{qV~spjrWMeN9(^_)j`dTTmaqhQ7sA z6C)j&5RWX>5wEfMOJrGA6A~W9cnVmD7LKa$UsdQUh3p9+o_2x1x$w|fSU-Y(rt$PE zFz$hl6S#UEF;YN}qvlPAhqfUXjsW#;@VE+|nvCn+U^A*ay9j(sfn+3*^q@Md3&tR1 z!)svgW7xZzZqh!)S2j3LMa{ZT)-7o7$^N22>^8j&ypw_W16CI?)(koEAY!{Ns{+YE zdJ=1e_>6kucjeBk6?PnTXaa9c5AzSH(d5{*I?YQz434{EVfsES{s_Dx2f4?-pHMvGW4 z=o(21b|BMeH}+%!x}VCk_NXhp!NXhhJnH=j=opH~?Ssf|gS=>f-AE-=&t&kfvNe>4 zdUFBQ@&KyOFUaK$h=DbTi&fMg-k*)AU4p1x2-ap`#WTpSl8DWnsB|CFXf~E!XCshh zqrmxmJiDBQFKVzG*e!2I_gO}7u-VA!5Al5#;&Tb|$3IqD7r>Z zJh${heQbqDDuw!x0WN+)cUp|tcn>qCqK?j|hOnqI%S9(kX9l%JO?wVKWfY?4CCu)I zI#rkb3J!K7UXOy=E6^1L@)))cIdKhMBvj2Zz*!f0)fAD{2=8>@RarLT@&!6bhS*AF z(c?}bvwmYo(NlA%Jx{>XT^)8Dx%ng_eFpu9okKrdEUF^9-0Zf+s>*7>f^Mjt(~v{IQYNbT8ARe?I?4{wNpyh?*nhtZRvttDy@Y%l2|ab` zL9Au{QPnv( z$wypVqrRxduOfQJgW(~Fs8)!qROr14Jv)GSIp%)`K2`wp4)A&nUh<-Q)rN1|Ac{I7 zc3b00ZPb_qRMi~HqGN??FQLy|K&QEY_eC=3669W{+8E8y+nOTtY9X$Z!EFe6aUQ(v z0sp^Z#xCrrj$=kP5XK^I??K&p1Uc6czJ8qD#2(`eW*&j%`_U8kA`1URb{(1c45H>LEDs1|pA{G?R=I%03nZ~%C=shufv7AnmIqtu$gZ;3Bb35t z5}5H|uV7mmB_XRo)wcv&#J(3KR_ za25L!H~NMb2s}XNLB@CxBOZ9pjdKOe1QE@5z^4<<1@M>!j5hdC!B2_8J(~}^t~8*k z0Yr6yvo2QMgS@YVnpg_*V^Di-)Po$bc?I4%jaWH}YH%8Pe*yjM8tT<;%+5uQhGDgW zt6tQk1dJ3^hE(Ka8Dwrb*joX+g9`9gSzt?n_hig5uq!=s8}AFya~#oi2$~MVb0@*d zWw4ZkdSgR}temA`V^v704S&}IJGG&?Do~^${}Z5F$I*g?d0_Q6n7IZ!FXQfIAh?RW zl;1N@KW^cE7H0V2J!zkgt`vtmsbIb=*sBCTRfmr1KwAlwLDr&pV3pBQP#9ZzSe1#q zy^UU+1zv*q&I;>O0gw=^_n@XFAZk)^RvH$Rf$UP4kpdJ6K;uQ-a-mwNV2DNtJM+LM{$^D; zFDV@|ijYL&m&-W?9u_y~{zYow}C?Bsu5{ zS(ufD>)E)Qha3yyEr38Tlw-hV8x>N47AfBYeIDR+fl(EF5m+?~@#Z0SatlWgM-#FL z-!xPQnRl^}5QkN=3V8A9!Z!}zScQDV6y}N_t7N`d*DyZ?I?6*r6=;x^DjjuHMx3<3DD0hH=yYMnpn!K7w+6fs zgyw=mtO>9t4(BdlwsFso&rI-g3)*i&Lsnswxq#3G3tY%N9o0oXTdL>|DsZd#rU13< z4Khz<9kk##*)2o(3_`QqDFzXzA=u@_s~5PXjN@=3dN;fjkg}jpcYz^np%)@U# zxFfSc;tdoc({Rq9+lEbY?`pyt3+tpTrO>Y)e9C?+yO-3X;yQzF%jSoX&lyB()N00PSbcC?VDqJahw&YY=E44eAb)b<|m17We*oB&8 z^^#~LD*OjU@GG^-jxTF)1gT@aL?;=QYo*tmm5v@Nw9<{0atA2=DfbNmR$3UcLWBwj z$GJoxbxM?yb!WffSSGbPavg8T)sib`aIA3lB+CD+a#lz^($_LmWJfabme$J*O@@v{ zuoe%?eE;P$h`Tb9GJ!zu33FhL>?J|0m9;z+NmTm&n0T5&=QARX_>?; ztElA3@uFNWnU@whu}e5gW}VoV`wh89T48`Y$wUAwNRJ1ATJDLZ?8)#>pCq*I{2qiL8&3QP=!5$`$G(9!4cD*lVt*?&uWogr~b&Yhf=bs>@qvQ9WTEaO1Nzmr!ox1{Ht z2qmDEvSo(LJ+_P|hj|zHkr697ih97oDkDYm>*Rv0d{UQ_^N#J(Dmf&_QidbP@n-bd z!O0Df30YC(PEBwyJLghzab6v~PNj47$a>;PcC^Z?ky(;oX}?n?iZUbP!0}b2$~mhg zZkZ#_OY$Y3U;G%-cFB1>hTNaYo+Ni!G7hDDKSnn2W&v?7IFVT`E4REWtA*?d@+new zLs<(YW{J<)8OT_2a7&L$Ju+sTxRL&lxFi!Y1_k2UE*x^_D6>lXTi%zk<|`aj7;{pO z%xkGf=9zrjk|T)WJl#1lBs-DBCaaCaDq}(7mTzf;)FWe5)@rlRXO3^B?QRSYhCG*k zb}FGW&xsZ1T>8?ip=B<7@lrC;UIsSMI9jvX=wB!3R~GA2YJPRU7BmxEhc;8c5w z$yx8@k7VEJP7b4yER_~JFL}2(MwypVm*dw+E;}sAHIiY+uhIN+dZnW)s>8x;$*Uv7 zktsQm@||Ab^i3&Gz9RW3`H|;x$d{u}>U2hQzGFuOTSSj!U*eD0=IDy7m8%_293)Ql zk~-xo`HE_fo;!D(&%fh~V00uqc%vAd8WZukQw#3aNT2=PZ;sC$8II)+UdPJf(c?&Sm@O`;sAnAH z5nawP;)|kd@1EgkF6zyQ7H7RiPYjpC$1dZB69wYHKHqm zH6pJlzPryIyz(=GA|j`#{D{t?>_(W4){_W7j!Z{hQJagONAyMJMG-hxieq(TN6<#r zMzA`^sQjWhrQE2j;uwqC9g$i57?D)`_`hLw?iJ-AI$PrWC;5NB`p@70`D$@_j$Y}T zh+pIt=|AT=#yLvg%9rFtR-VW{uJ{@!{-f2-$rdTk@qLt6iA=tvo=DsjjhToZr^-cr zDX~jV{;oq_cl1Q_EK(u=Ntaw7;nrCfwO`ugk&Tw&WXG(U&Zyw70&DL{)~K&+>hvRY;x{Oc@bM8M|n-Yq#n7p_`3g1SJ8Em z_0gkq_n$r^2=6{dS3CSkj747##-g(#=l_03&WOGu^NUKo`~2UfIg+AM?w`C7|I^*#b{3xzWk{Yyq(!ert`t8;B}Ug2pHWoW-|xup z|Gq9N``^x-wNZ>k&x-3RF5zySMOWT^*O3*K@ozKa^?!R6*YMBZ{%vjKx}*2s*8a2h z{|#$&#lO#rYAeoYbZrD(RgWcJ^W(ba#~=g9c`)xUrL_w~`4j?RCcSrln> zMPzPqw2`ZKAEQ$4mQi$eR9?}m|9<}e#(|?VDm!v^x7;Yszn>My9g*MfLm} z&bze~UG;xEzk7A@D|e%f+`s!yM3>V;Bj4mPvgYpNzey?1XmnOlsm^(H=H1exSE3Rk Z^P`d?SN`dE_c~`*RC3WXXLeNb{{gj=wiy5b literal 0 HcmV?d00001 diff --git a/Copy/Settings/GeneralSettings.swift b/Copy/Settings/GeneralSettings.swift index 7ec15a6..805d6ae 100644 --- a/Copy/Settings/GeneralSettings.swift +++ b/Copy/Settings/GeneralSettings.swift @@ -26,7 +26,7 @@ struct GeneralSettings: View { Text("Standard").tag(false) Text("Compact").tag(true) } - .pickerStyle(.segmented) + .pickerStyle(.menu) } footer: { Text("Compact shows smaller cards so more fit in the shelf at once.") .font(.footnote) @@ -55,6 +55,28 @@ struct GeneralSettings: View { .foregroundStyle(.secondary) } + Section { + HStack { + Text("Copy Sound") + Spacer() + Picker("Copy Sound", selection: $settings.copySound) { + ForEach(CopySound.allCases) { sound in + Text(sound.title).tag(sound) + } + } + .labelsHidden() + .pickerStyle(.menu) + .frame(width: 120) + } + .onChange(of: settings.copySound) { _, sound in + CopySoundPlayer.shared.play(sound) + } + } footer: { + Text("Plays after Copy captures a new clipboard item.") + .font(.footnote) + .foregroundStyle(.secondary) + } + Section { Toggle("Launch at Login", isOn: launchAtLoginBinding) } footer: { diff --git a/Copy/Settings/SettingsStore.swift b/Copy/Settings/SettingsStore.swift index f5acc75..dcad950 100644 --- a/Copy/Settings/SettingsStore.swift +++ b/Copy/Settings/SettingsStore.swift @@ -1,6 +1,32 @@ import Foundation import Observation +/// Optional feedback played after Copy successfully records a clipboard change. +/// Raw values are persisted in UserDefaults, so keep them stable across releases. +enum CopySound: String, CaseIterable, Identifiable { + case off + case bubblePop + case clickTone + + var id: Self { self } + + var title: String { + switch self { + case .off: return "Off" + case .bubblePop: return "Bubble" + case .clickTone: return "Click" + } + } + + var resourceName: String? { + switch self { + case .off: return nil + case .bubblePop: return "copy-bubble-pop" + case .clickTone: return "copy-click-tone" + } + } +} + /// How long unfavorited, unpinned history items are kept before pruning. enum RetentionPeriod: String, CaseIterable { case unlimited @@ -57,6 +83,7 @@ final class SettingsStore { static let shelfProDarkKey = "shelfProDark" static let hideMenuBarIconKey = "hideMenuBarIcon" static let doubleClickToPasteKey = "doubleClickToPaste" + static let copySoundKey = "copySound" var retention: RetentionPeriod { didSet { @@ -154,6 +181,15 @@ final class SettingsStore { } } + /// Sound feedback for successful clipboard captures. Off is the intentional + /// default so installing or updating Copy never adds noise without consent. + var copySound: CopySound { + didSet { + guard copySound != oldValue else { return } + defaults.set(copySound.rawValue, forKey: Self.copySoundKey) + } + } + @ObservationIgnored var onRulesChange: ((Set) -> Void)? /// Fired by the About pane's "Check for Updates…" button. Bridged to Sparkle's /// `updaterController` in `AppDelegate` (which owns it), so this store — and the @@ -191,6 +227,8 @@ final class SettingsStore { shelfProDark = (defaults.object(forKey: Self.shelfProDarkKey) as? Bool) ?? false hideMenuBarIcon = (defaults.object(forKey: Self.hideMenuBarIconKey) as? Bool) ?? false doubleClickToPaste = (defaults.object(forKey: Self.doubleClickToPasteKey) as? Bool) ?? true + copySound = defaults.string(forKey: Self.copySoundKey) + .flatMap(CopySound.init(rawValue:)) ?? .off if let data = defaults.data(forKey: Self.excludedBundleIDsKey), let decoded = try? JSONDecoder().decode([String].self, from: data) { excludedBundleIDs = decoded.sorted() diff --git a/Copy/Support/CopySoundPlayer.swift b/Copy/Support/CopySoundPlayer.swift new file mode 100644 index 0000000..0ce473b --- /dev/null +++ b/Copy/Support/CopySoundPlayer.swift @@ -0,0 +1,57 @@ +import AppKit + +extension Notification.Name { + /// Posted after Copy itself successfully places a selected shelf item on the + /// pasteboard. The clipboard monitor ignores that marked write, so this separate + /// event avoids both missed feedback and double playback. + static let copyDidCompleteInternalCopy = Notification.Name( + "com.tarikbc.copy.didCompleteInternalCopy") +} + +/// Small cached player for optional clipboard-capture feedback. `NSSound` is enough for +/// these sub-second bundled WAVs and follows the user's system output device and volume. +@MainActor +final class CopySoundPlayer { + static let shared = CopySoundPlayer() + + private var cache: [CopySound: NSSound] = [:] + private var playing: NSSound? + private var internalCopyObserver: NSObjectProtocol? + + private init() { + internalCopyObserver = NotificationCenter.default.addObserver( + forName: .copyDidCompleteInternalCopy, + object: nil, + queue: .main + ) { [weak self] _ in + MainActor.assumeIsolated { + self?.playPersistedSelection() + } + } + } + + func play(_ choice: CopySound) { + playing?.stop() + playing = nil + + guard let resourceName = choice.resourceName else { return } + let sound: NSSound + if let cached = cache[choice] { + sound = cached + } else { + guard let url = Bundle.main.url(forResource: resourceName, withExtension: "wav"), + let loaded = NSSound(contentsOf: url, byReference: false) else { return } + cache[choice] = loaded + sound = loaded + } + + playing = sound + sound.play() + } + + private func playPersistedSelection() { + let choice = UserDefaults.standard.string(forKey: SettingsStore.copySoundKey) + .flatMap(CopySound.init(rawValue:)) ?? .off + play(choice) + } +} From 6a9774e25aff1c9cc6f1a5d77f1adbd204dd8552 Mon Sep 17 00:00:00 2001 From: Tarik Caramanico Date: Thu, 10 Sep 2026 22:09:32 -0300 Subject: [PATCH 2/2] fix: use the macOS system sounds, and keep the change to sound alone The two bundled WAVs are gone. Both source licences (Pixabay and Mixkit) forbid handing their audio over as a standalone file, and a file committed to a public repository is downloadable on its own, so shipping them from a GPL-3.0 tree was a licence its own terms could not carry. `NSSound(named:)` reaches the sounds macOS already ships, which costs nothing in the bundle and follows the listener's output device and alert volume. Pop and Tink stand in for Bubble and Click; adding another is one case in the enum. That also removes SOUND_LICENSES.md, which XcodeGen was sweeping into Contents/Resources and shipping inside the app. Drops the notification hop. `copyToClipboard` calls the player directly, so the player no longer reads UserDefaults.standard behind SettingsStore's back to answer an event, and nothing has to be woken early to listen. Restores the Shelf Size picker to .segmented. It was changed to .menu in passing and has nothing to do with sound. --- Copy/App/AppCoordinator.swift | 7 ++-- Copy/Resources/Sounds/SOUND_LICENSES.md | 34 ------------------ Copy/Resources/Sounds/copy-bubble-pop.wav | Bin 19838 -> 0 bytes Copy/Resources/Sounds/copy-click-tone.wav | Bin 17758 -> 0 bytes Copy/Settings/GeneralSettings.swift | 2 +- Copy/Settings/SettingsStore.swift | 17 +++++---- Copy/Support/CopySoundPlayer.swift | 42 ++++++---------------- CopyCore/Package.resolved | 20 ++++++++++- 8 files changed, 43 insertions(+), 79 deletions(-) delete mode 100644 Copy/Resources/Sounds/SOUND_LICENSES.md delete mode 100644 Copy/Resources/Sounds/copy-bubble-pop.wav delete mode 100644 Copy/Resources/Sounds/copy-click-tone.wav diff --git a/Copy/App/AppCoordinator.swift b/Copy/App/AppCoordinator.swift index 0c1a071..69abd39 100644 --- a/Copy/App/AppCoordinator.swift +++ b/Copy/App/AppCoordinator.swift @@ -356,10 +356,6 @@ final class AppCoordinator { } let settings = SettingsStore() self.settings = settings - // Start listening before the shelf can perform an explicit Cmd+C. That path - // marks its pasteboard write as internal, so the clipboard monitor correctly - // ignores it and cannot be the source of sound feedback. - _ = CopySoundPlayer.shared let linkFetcher = LinkMetadataFetcher(store: store) let ocrController = OCRController(store: store) let reporter = saveErrors @@ -621,6 +617,9 @@ final class AppCoordinator { } pasteService.place(reps, plainTextOnly: false) try? store.touch(itemID: id) + // The monitor ignores our own marked write, so the capture path below never + // sees this copy and the feedback has to be asked for here. + CopySoundPlayer.shared.play(settings.copySound) return true } diff --git a/Copy/Resources/Sounds/SOUND_LICENSES.md b/Copy/Resources/Sounds/SOUND_LICENSES.md deleted file mode 100644 index 7871071..0000000 --- a/Copy/Resources/Sounds/SOUND_LICENSES.md +++ /dev/null @@ -1,34 +0,0 @@ -# Copy sound assets - -The two WAV files in this directory are trimmed and loudness-matched derivatives -prepared for Copy. Each output is stereo 44.1 kHz PCM with an overall RMS level of -approximately -25.5 dBFS. - -## Bubble pop - -- File: `copy-bubble-pop.wav` -- Author: Universfield -- Source: "Bubble Pop 04" on Pixabay -- Source URL: https://pixabay.com/sound-effects/people-bubble-pop-04-323580/ -- License: Pixabay Content License -- Original SHA-256: `9cffab062f297d47bf9a8b8807b935b9cdb887562e6231fb7cf78763422fcc44` - -## Click tone - -- File: `copy-click-tone.wav` -- Source: "Cool interface click tone" on Mixkit -- Source URL: https://mixkit.co/free-sound-effects/interface/ -- License: Mixkit Sound Effects Free License -- Original SHA-256: `248312c2ef619427ef7024126a846e4210fe37e39038cfd32c73b1fb854a8086` - -License references: - -- Pixabay: https://pixabay.com/service/license-summary/ -- Mixkit: https://mixkit.co/license/ - -## Processing - -Each source was trimmed to its short audible transient, converted to stereo -44.1 kHz 16-bit PCM, and gain-adjusted to approximately -25.5 dBFS RMS. The -bundled WAV files are derivatives intended only for use as Copy's UI feedback; -they are not distributed as standalone stock audio. diff --git a/Copy/Resources/Sounds/copy-bubble-pop.wav b/Copy/Resources/Sounds/copy-bubble-pop.wav deleted file mode 100644 index e0da1988613d117b0b7d1f6a7fb178f6b9dbbd03..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19838 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 diff --git a/Copy/Resources/Sounds/copy-click-tone.wav b/Copy/Resources/Sounds/copy-click-tone.wav deleted file mode 100644 index 82381a7678df53c320fc3fa5a3cf2ff09bae5430..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17758 zcmb7q2Y?hs*KJjYNt<_B!U9W9N(K>B1WAe@d?blVP(egQlxzS*FcL&SM1o2X6a-N~ z0YyNRC{c1wOWHg;p+ntwYWtgic18d9-V8O}(_MA%x#yhu5rw{CwTVyDHo3kxpJbCln{Ab9(ra2jDrvP3AI!Xz zJ^Ff=oAa)p%h+-2-CL#er)3W{mgLV4#roUjJ&=EYL66`IYD?v(^Aq=MVN+QoGm|CC8*rNl&kkQmad?oegF+ zsL*8ceTj|I8!f1wd{5hwgUkDT4U&G@)NWR#LXA@8cBLmLTul1hdXlfb^=;0$^IfjQoospf^s#Zr>?1!P8hm)(pXU!9 z{WEy1%h8b+JDuB|Gw}8XHo)i+cg#JzBqgse|7y8SRjKNOwa3+dzrpicjQ~4PGh`{EX6>c{?U+v=ee;qiX9<`3D$N66;&i;7n`qk$y zmb|kg<9yz^?49O!;cf20`h*grl0HoTploiH+)A%k9awdBrFB&*r9WD!AZ=gysU<%w zy)1ru;t(~gmkJIxrrt5L_gw38BlXI6SKhhY?Nas2Z7w#tJm=!Pt1YkWxK-}vtN#4l z-PFa(^|o|rNj>9-rA{ajDBZiv=yEN}UrwJ}VODy_3f;ZMANMyQ@R^Vc#p> zJIPCuDx_^JJv4oA`Jd7+rTfr=Dj(8 zW_)`y<=U2OXRg+`8obu->eh@+*CyXueB=J?QJEtG{rx@Y2TSu*(ND*}7&|0IO@64P zt7O&EJ4<&f^FUgOGCZwo$qA)?PJA!9qxVG2QvR+cjQRG4g4cqra?|q4W)8}nbaTng zDH(5N)V%S1#^T#OZkai!v$6uc{gbUy#uxlB9dmu5_ltco=A)$5iOo_cml#^|k&*}2pKO*k=#F~kXlGY}5OsbtcKXGQ#r|}CDa(&ff zPwOwZ>oA|f%_CMpphGA(f4+ZV?)u#Q*|V}wWR1@1p8b2)%G|9v^9ySFI~o&0mqolC zQeNk)Twm)oeII)V#%XZ}6XquDOx&M%F5#KPK5^sYL*6MdeO=u1i_%DARD&@y%M1n% z2504W_fO6($o1rgax!yU=B~(pH}81ChyLNACczcvbYnhE5x3P{$`$u-u0y_%cWi9= zSQghLZeQ&1I1zIwcCxQ`Oq|E>`BGb}uV9z>A6BfGAO6bd9Lx{yF7Ov@_s{b;^Iypy z>-YJO7Bnk(A~ZbsmN~$f<`>neTbd*V$Jy=DwJ{zLGJIdT)5M zT<6`7Xnpl1e3ep`a_LK}h27X#ZzPAthdv9O3zQF(3oHt>4x9Z#%R*>l1(+;h^i*4^IozN^0bx;9<^MH!|xW;txJJxB~O zQ>~-nw#K;-h5Ch>hVBn#1gC_`hlYmpLY6Vqs9-;Dy+PlK4Lpa9{4RdQ8x z-|L?1-sm3SUhIC}bT2(`x0@-})5ct*Pk3B7Cv+?HKv;yz z87srjn12`-toy97;*@<4TSh@Xg>P27tBv#vS~XWtALE+jdcgIj>tkJWeWBIRhpIX1 zbv{(7%vP|+#R0L=E`xs!pT$bylEE zRw`+yRYSX~y{pgHx9fNG_x1hy5v{hqL@lM2QC2FWn3rD_Lus;o&F)~mZ#`xXF=rZc zj5d(`hSAn&W}Y&7S|`nQb|t$a%@y-l1s0=x!(UV@sdKe~T0gy=zFgm+57$@eyS01u z<7yXel~P-Mg74+~s0C{-z7l(^YWC;mCG(PDLh@weTVteA!Q5qBF~^x9YpT^toVRb# z3-l(}`Ay|*#iRYM9?&*u&Gk-tcl~WWR_~$r)qc|2shiX*{28S;yTFc%uJoFH-tK9A zVvRIsn5T`uj3ve-e12+7GOL@@t3sy?m$a|=X>Hw{Yma2R7r}bC# z`ug+w94$v%3NA(~1xf|Jh_9ontfg2cZdni52Tiw?Xx22h8S9MG#!{o3X&dR*_htio zt2I%$L;+2tSAqG6GDgW#*Q!5g3$!%7rd}1O4{7D}irPf2jQWnchqqR`u!HQRc!DO{ z7w!Jm5^I{d6w*S*VPlQqGxr&5%-%9~na zy`%2aTkE5>{n|7&p!QUbDvZCu=Ti)8B<6`MV7_Q3TQ$rF%p=Bc#+SxV#>{X@b8+~v zIU{`5nqXEF!`K}9N>AiNJ>!%so;B(-o;g}KcLn`~tBvmV==wR&bK2{!;p!1yPnl$I zXM>Ek)IW6EUJ+bmH3=>=TNQ9~Sl-XZr@0pm<{xTS33aiySyOBm&k*_Q)9i@8k5Bca zsPkhhYONB_Ydhlwt>b`^m zZFO8%ZL6=hTFaHL^ko~^3q}h%RdB+to4?R{H+O}Zn&URr>^(+KR<7}G&g*77|1;LN z;g9W>lrQ$H&$Fl9NBIn2X?0;-MXi7039WTPNrksnS`_S@R48aBc803+Qg$V+6W{4Rt8I%}=+22h?CqHNd(47_N8_H1 zTNBsY_ik)ox8M7y_L^%vJEH70Cs0;kl65+7LAYzqkAXGWt^AX6{>n3R&*mM<|BruU zV0)meF+1FxhFY&{1F5lhm9i+dn(M9jUf!n@zKvO%@MCQIgyFFZ;u^&C^NsiJ)t_}Q zVmq~;&DZ#$;12tcKh@}$7atsuQ?{UG_Cx-e+132t<^ht9{>S8L{o$aq-W3 zpN;?0M{#?7KgI^VkNd`Ywzy`x-d1{Roy1|j%&^6e!4&KDf-&KJ`R@jQ$(tBho7W+b zmA^SKEU+TDKAaT3-%d6^X3Okl>bq>btCyPM{a&vV^Q^m5>_|_|*aFXPU)b}3=NZpc zJ;_~4RrQAKRi%o(m2{)6-67cA99Hmoc%6SxsAR$HV7EZ;U}ES>@E)T{_&ck*xq|xJ ze<`2QaeWhS;`vrxaT;16>1A|GF`c+Rm;4%nfOEsHr_5 zIMDnekQIJ0kQ%-axE`t;dMsSuNHPAj?ltGohgNl^q3Eq8uv)G>zR{`66)rqCUk6##5#fQfC_HLt$-QE1tYH1y@X4)xsHJV`m!A!fU@|ozV_M>#IDO;!| z^Ap-0{)^UDX{x=gJffacdMh){$-!a%<2j+gTLmwBE@}^#{k+5n>HA9mocKq=E;UKd zrfVW2G(B+o#?v>ioSAhY?tJ}AdAAep#0E+ST85`vwRxtVkhm%Ny|mJ084#eU(_yiN6FrH)Z8RK@>x-rUf2e?PX!$l*QgvTUe$U%jf7^BwRk^zyYIM;6$ib?gReKw|~#>k$Ed;df;a8AkC%+v;msx`PTDt!iD(jsWVgjCAue% zi8~w9!gW!5L|IPt?QP+&{4sesnG^0jly&6J-2BbCkA z<{Rvb^HueIccZ`YQuWijbz~7)$ zeX=smRZCmxTCP=gb=O|k@|1SG3wzQoYqv7qF@6cP4EG3C2=58`j2T8_>w>+A_VH2b z9c`X#pX&|JK6fi`j{CZ2oh#LSOTDDdXQkO`dy{3GO^lz7g`uuSl~5D2e7L*)l(~~? zh!y+?c3#<{%u{P>bJZWUK}xJvmvvCi*>zY|tFtI$-mz91<*dqPP3x4^+J2d8Q)#6w zZ>}Y%f9eCZUtB5rGS?}sl|BWL(2sX#KJp1}SGHa;cN)`;LB`icP4l$5-8yK`6&}`& zRaI{AJ8GI*MxUs5({q$JwIBE?Hx>P?ruN6?C2N*h!ERxFXTM;#7nejbjbVo=m(OIe z>WlmnwY2iFT2V<-$M7o3XjZ`9q(8_@m&JG*EuLX72_Ii89^ut#5Z_84@J?(N|Auwv zo7pZliG4*0On|wktgYf@vkvytWXD@AMLGMpXe*Y{2%61~vT1yx(nU$ub}JtJh|)lR zMOmV4=3~`gSfVnYPO(Yi7X5(VA3E5HqK)0go^Kzw9u^r^It{lISSfLly)DM_#o`aX zQXJrC#7zDWmEnKT2ka#l&s6>cy~N+4s~8_CKTucIqyuUzwoM(*N~ptFHgC@6vTO7x zy+ozNZ4t62h@ja_v@{vctsLW4VUMudYzsR{UHMYV=aXoH z(t-X^EU`-oim^&by2D?l`}i&D#hS4})RYYtHchj4(rxQk`p!yXXRZ0{DcjAHMQwgr zROFi}j(^AktUce&PVr}1ZDkGJudEXuWrw|qW53Dk*@=7#I#8UL$kvNS>}9$}z1cz< z$$Qe#%{! zH{@4p(SzznQC{sQzEY|QRVgd_^T)-0wobHVZD=dy(7iODEfH0CWAPTOS<5fktNAJW zb$-;YisO2wi6-ntv5S(Bw`ZsYea#xt5Z;nJN?&Bw1S&wbrmO2IPuWG!EBoQ~-SjE@ znU>Q}bV&S8Rm5%j+^&kg^gL@}uVj<$)66SEY>TkiOLUpVu$62Y>%y+H>(rEwr!l-N z&Eprv4F0DW!mo*jyc{t;oKCR)w2IYX&#@_t=vOw4&aebJ!oC&Xpf9&%Ekrd@SFA8w zh_>cLv0DU%Tl<0%-Tm1pS2B;&7xVp^!RM+~l?{}x)G@E}@Zu(bJ@F!%GYBoQZm_a zWgGCm$DUxh;-IK$W!N@;9m}xZuq*Nz_D!vxh;@ymM*458llCdEruun*R!Lc4r6}>n zD1Ii~o*gh!X^PoVd~IE{8`BqdJ!Pf6TWujeQoGYNX5l&6MIERNJ0dJTL5x#Jh)L>MF;!VgEBSNmHhzs6PhI$Udj`M9n!wv5-tV*1 zSv;PWIjs_F*}dW$<-C1Fy<=~{T1#n8ZImQdm+xoYXax3aH+f6Tr?jo--*u3kK!Z6OP?rH=n=)sI`i2qNJ;!SqP3M6#p6X&9*Y|AyyyfRztKDFVZ`(~ zk-@hLML8qRp!&As?^8Ye+Mg+2V*&dV8)hr~K0A+fvwvm<_Dd{{;@I=(JFoG6)J$oC zr=N~=NEu5*l^^L{?nRI7%RWPe%n-}j5HW)_6MfJx6IoT-OlRqNdWAJcX4R$|yaQE4 z2i(BUl8#?Hy3!K1Q(R;Hg^sFaBX15eA*QecREaHMTj_EB0v+MaC{3w>9eYih%-hiK zY&8C6@DsI0SN}>p$2uUAI*6~>Qt>vj;4$_m#j@cnhk`6XPjH3(2Rj!xjvwRi7kKJm zA5bgQu1@Ttc$nQ1Zmju%KB2)_bw5pF_aJ|ok%#x9Og4!Yvn|vJ>$cGZ)|t+t%ar3+ zM1Af-XMYMakI-EF{?P%u2bDEMhwse?Qwx3%<*|5ri>MFu=|eV;o?^?X8tl4FUbdDx zv*9!YT>ijBMeL|{VFyu$CDLKfT8v76V_!m?H3>eU`6IEam z@IK7WGcUefp!ZN4o3q9E4f8G9h?+W!hSOvkMeoyVw36PWqx1%GHW3JBpmwZ7yk4cc zES<%&p6oJe(01C0tEbs_u=+RhV-ItJrLu*r9qY(mMkKz6?(!jf0SrDwU06jb$xvLtYQo92K__)7iby zQ;r=Z6*+Q&4gvWE`Up0R0uO`8&H9qY`csfzrOWgw?S;mTR0>G?vDGvW7H?(e=>YSw zz39~&P{Th$&F{xPVddFuFh2o|cfuaCG#duFLuobSOa(t9fqEpZqqpe*em}hdy$sbh ziB-nB=7_DwA?-2N8Rvt6a6Bu8-r!+xp+Ahl7|7h%L!`jE3hWAg=l%vcJdV3^?hvyO;!^rHi%*9$Ef`2dojdV zFV+^`YX>j3WUsKM>?u|cS8K6ytU7qB$}Yja{qV~spjrWMeN9(^_)j`dTTmaqhQ7sA z6C)j&5RWX>5wEfMOJrGA6A~W9cnVmD7LKa$UsdQUh3p9+o_2x1x$w|fSU-Y(rt$PE zFz$hl6S#UEF;YN}qvlPAhqfUXjsW#;@VE+|nvCn+U^A*ay9j(sfn+3*^q@Md3&tR1 z!)svgW7xZzZqh!)S2j3LMa{ZT)-7o7$^N22>^8j&ypw_W16CI?)(koEAY!{Ns{+YE zdJ=1e_>6kucjeBk6?PnTXaa9c5AzSH(d5{*I?YQz434{EVfsES{s_Dx2f4?-pHMvGW4 z=o(21b|BMeH}+%!x}VCk_NXhp!NXhhJnH=j=opH~?Ssf|gS=>f-AE-=&t&kfvNe>4 zdUFBQ@&KyOFUaK$h=DbTi&fMg-k*)AU4p1x2-ap`#WTpSl8DWnsB|CFXf~E!XCshh zqrmxmJiDBQFKVzG*e!2I_gO}7u-VA!5Al5#;&Tb|$3IqD7r>Z zJh${heQbqDDuw!x0WN+)cUp|tcn>qCqK?j|hOnqI%S9(kX9l%JO?wVKWfY?4CCu)I zI#rkb3J!K7UXOy=E6^1L@)))cIdKhMBvj2Zz*!f0)fAD{2=8>@RarLT@&!6bhS*AF z(c?}bvwmYo(NlA%Jx{>XT^)8Dx%ng_eFpu9okKrdEUF^9-0Zf+s>*7>f^Mjt(~v{IQYNbT8ARe?I?4{wNpyh?*nhtZRvttDy@Y%l2|ab` zL9Au{QPnv( z$wypVqrRxduOfQJgW(~Fs8)!qROr14Jv)GSIp%)`K2`wp4)A&nUh<-Q)rN1|Ac{I7 zc3b00ZPb_qRMi~HqGN??FQLy|K&QEY_eC=3669W{+8E8y+nOTtY9X$Z!EFe6aUQ(v z0sp^Z#xCrrj$=kP5XK^I??K&p1Uc6czJ8qD#2(`eW*&j%`_U8kA`1URb{(1c45H>LEDs1|pA{G?R=I%03nZ~%C=shufv7AnmIqtu$gZ;3Bb35t z5}5H|uV7mmB_XRo)wcv&#J(3KR_ za25L!H~NMb2s}XNLB@CxBOZ9pjdKOe1QE@5z^4<<1@M>!j5hdC!B2_8J(~}^t~8*k z0Yr6yvo2QMgS@YVnpg_*V^Di-)Po$bc?I4%jaWH}YH%8Pe*yjM8tT<;%+5uQhGDgW zt6tQk1dJ3^hE(Ka8Dwrb*joX+g9`9gSzt?n_hig5uq!=s8}AFya~#oi2$~MVb0@*d zWw4ZkdSgR}temA`V^v704S&}IJGG&?Do~^${}Z5F$I*g?d0_Q6n7IZ!FXQfIAh?RW zl;1N@KW^cE7H0V2J!zkgt`vtmsbIb=*sBCTRfmr1KwAlwLDr&pV3pBQP#9ZzSe1#q zy^UU+1zv*q&I;>O0gw=^_n@XFAZk)^RvH$Rf$UP4kpdJ6K;uQ-a-mwNV2DNtJM+LM{$^D; zFDV@|ijYL&m&-W?9u_y~{zYow}C?Bsu5{ zS(ufD>)E)Qha3yyEr38Tlw-hV8x>N47AfBYeIDR+fl(EF5m+?~@#Z0SatlWgM-#FL z-!xPQnRl^}5QkN=3V8A9!Z!}zScQDV6y}N_t7N`d*DyZ?I?6*r6=;x^DjjuHMx3<3DD0hH=yYMnpn!K7w+6fs zgyw=mtO>9t4(BdlwsFso&rI-g3)*i&Lsnswxq#3G3tY%N9o0oXTdL>|DsZd#rU13< z4Khz<9kk##*)2o(3_`QqDFzXzA=u@_s~5PXjN@=3dN;fjkg}jpcYz^np%)@U# zxFfSc;tdoc({Rq9+lEbY?`pyt3+tpTrO>Y)e9C?+yO-3X;yQzF%jSoX&lyB()N00PSbcC?VDqJahw&YY=E44eAb)b<|m17We*oB&8 z^^#~LD*OjU@GG^-jxTF)1gT@aL?;=QYo*tmm5v@Nw9<{0atA2=DfbNmR$3UcLWBwj z$GJoxbxM?yb!WffSSGbPavg8T)sib`aIA3lB+CD+a#lz^($_LmWJfabme$J*O@@v{ zuoe%?eE;P$h`Tb9GJ!zu33FhL>?J|0m9;z+NmTm&n0T5&=QARX_>?; ztElA3@uFNWnU@whu}e5gW}VoV`wh89T48`Y$wUAwNRJ1ATJDLZ?8)#>pCq*I{2qiL8&3QP=!5$`$G(9!4cD*lVt*?&uWogr~b&Yhf=bs>@qvQ9WTEaO1Nzmr!ox1{Ht z2qmDEvSo(LJ+_P|hj|zHkr697ih97oDkDYm>*Rv0d{UQ_^N#J(Dmf&_QidbP@n-bd z!O0Df30YC(PEBwyJLghzab6v~PNj47$a>;PcC^Z?ky(;oX}?n?iZUbP!0}b2$~mhg zZkZ#_OY$Y3U;G%-cFB1>hTNaYo+Ni!G7hDDKSnn2W&v?7IFVT`E4REWtA*?d@+new zLs<(YW{J<)8OT_2a7&L$Ju+sTxRL&lxFi!Y1_k2UE*x^_D6>lXTi%zk<|`aj7;{pO z%xkGf=9zrjk|T)WJl#1lBs-DBCaaCaDq}(7mTzf;)FWe5)@rlRXO3^B?QRSYhCG*k zb}FGW&xsZ1T>8?ip=B<7@lrC;UIsSMI9jvX=wB!3R~GA2YJPRU7BmxEhc;8c5w z$yx8@k7VEJP7b4yER_~JFL}2(MwypVm*dw+E;}sAHIiY+uhIN+dZnW)s>8x;$*Uv7 zktsQm@||Ab^i3&Gz9RW3`H|;x$d{u}>U2hQzGFuOTSSj!U*eD0=IDy7m8%_293)Ql zk~-xo`HE_fo;!D(&%fh~V00uqc%vAd8WZukQw#3aNT2=PZ;sC$8II)+UdPJf(c?&Sm@O`;sAnAH z5nawP;)|kd@1EgkF6zyQ7H7RiPYjpC$1dZB69wYHKHqm zH6pJlzPryIyz(=GA|j`#{D{t?>_(W4){_W7j!Z{hQJagONAyMJMG-hxieq(TN6<#r zMzA`^sQjWhrQE2j;uwqC9g$i57?D)`_`hLw?iJ-AI$PrWC;5NB`p@70`D$@_j$Y}T zh+pIt=|AT=#yLvg%9rFtR-VW{uJ{@!{-f2-$rdTk@qLt6iA=tvo=DsjjhToZr^-cr zDX~jV{;oq_cl1Q_EK(u=Ntaw7;nrCfwO`ugk&Tw&WXG(U&Zyw70&DL{)~K&+>hvRY;x{Oc@bM8M|n-Yq#n7p_`3g1SJ8Em z_0gkq_n$r^2=6{dS3CSkj747##-g(#=l_03&WOGu^NUKo`~2UfIg+AM?w`C7|I^*#b{3xzWk{Yyq(!ert`t8;B}Ug2pHWoW-|xup z|Gq9N``^x-wNZ>k&x-3RF5zySMOWT^*O3*K@ozKa^?!R6*YMBZ{%vjKx}*2s*8a2h z{|#$&#lO#rYAeoYbZrD(RgWcJ^W(ba#~=g9c`)xUrL_w~`4j?RCcSrln> zMPzPqw2`ZKAEQ$4mQi$eR9?}m|9<}e#(|?VDm!v^x7;Yszn>My9g*MfLm} z&bze~UG;xEzk7A@D|e%f+`s!yM3>V;Bj4mPvgYpNzey?1XmnOlsm^(H=H1exSE3Rk Z^P`d?SN`dE_c~`*RC3WXXLeNb{{gj=wiy5b diff --git a/Copy/Settings/GeneralSettings.swift b/Copy/Settings/GeneralSettings.swift index 805d6ae..56d1784 100644 --- a/Copy/Settings/GeneralSettings.swift +++ b/Copy/Settings/GeneralSettings.swift @@ -26,7 +26,7 @@ struct GeneralSettings: View { Text("Standard").tag(false) Text("Compact").tag(true) } - .pickerStyle(.menu) + .pickerStyle(.segmented) } footer: { Text("Compact shows smaller cards so more fit in the shelf at once.") .font(.footnote) diff --git a/Copy/Settings/SettingsStore.swift b/Copy/Settings/SettingsStore.swift index dcad950..00a651e 100644 --- a/Copy/Settings/SettingsStore.swift +++ b/Copy/Settings/SettingsStore.swift @@ -1,28 +1,31 @@ +import AppKit import Foundation import Observation /// Optional feedback played after Copy successfully records a clipboard change. /// Raw values are persisted in UserDefaults, so keep them stable across releases. +/// Each case names a sound macOS ships in `/System/Library/Sounds`; adding another of +/// those (Bottle, Glass, Morse, Purr...) is one case here and nothing else. enum CopySound: String, CaseIterable, Identifiable { case off - case bubblePop - case clickTone + case pop + case tink var id: Self { self } var title: String { switch self { case .off: return "Off" - case .bubblePop: return "Bubble" - case .clickTone: return "Click" + case .pop: return "Pop" + case .tink: return "Tink" } } - var resourceName: String? { + var systemSoundName: NSSound.Name? { switch self { case .off: return nil - case .bubblePop: return "copy-bubble-pop" - case .clickTone: return "copy-click-tone" + case .pop: return "Pop" + case .tink: return "Tink" } } } diff --git a/Copy/Support/CopySoundPlayer.swift b/Copy/Support/CopySoundPlayer.swift index 0ce473b..eee84b6 100644 --- a/Copy/Support/CopySoundPlayer.swift +++ b/Copy/Support/CopySoundPlayer.swift @@ -1,57 +1,35 @@ import AppKit -extension Notification.Name { - /// Posted after Copy itself successfully places a selected shelf item on the - /// pasteboard. The clipboard monitor ignores that marked write, so this separate - /// event avoids both missed feedback and double playback. - static let copyDidCompleteInternalCopy = Notification.Name( - "com.tarikbc.copy.didCompleteInternalCopy") -} - -/// Small cached player for optional clipboard-capture feedback. `NSSound` is enough for -/// these sub-second bundled WAVs and follows the user's system output device and volume. +/// Plays the optional feedback for a clipboard capture. The sounds are the ones macOS +/// already ships in `/System/Library/Sounds`, so Copy bundles no audio of its own: a +/// stock-audio licence that permits redistribution from a public GPL repository is hard +/// to satisfy (both Pixabay and Mixkit forbid handing their files over on their own, and +/// a file in this repo is downloadable on its own), and a system sound already follows +/// the listener's output device and alert volume. @MainActor final class CopySoundPlayer { static let shared = CopySoundPlayer() private var cache: [CopySound: NSSound] = [:] private var playing: NSSound? - private var internalCopyObserver: NSObjectProtocol? - private init() { - internalCopyObserver = NotificationCenter.default.addObserver( - forName: .copyDidCompleteInternalCopy, - object: nil, - queue: .main - ) { [weak self] _ in - MainActor.assumeIsolated { - self?.playPersistedSelection() - } - } - } + private init() {} func play(_ choice: CopySound) { + // A burst of copies should sound like the latest one, not like all of them. playing?.stop() playing = nil - guard let resourceName = choice.resourceName else { return } + guard let name = choice.systemSoundName else { return } let sound: NSSound if let cached = cache[choice] { sound = cached } else { - guard let url = Bundle.main.url(forResource: resourceName, withExtension: "wav"), - let loaded = NSSound(contentsOf: url, byReference: false) else { return } + guard let loaded = NSSound(named: name) else { return } cache[choice] = loaded sound = loaded } - playing = sound sound.play() } - - private func playPersistedSelection() { - let choice = UserDefaults.standard.string(forKey: SettingsStore.copySoundKey) - .flatMap(CopySound.init(rawValue:)) ?? .off - play(choice) - } } diff --git a/CopyCore/Package.resolved b/CopyCore/Package.resolved index 6facdf9..2e67ef0 100644 --- a/CopyCore/Package.resolved +++ b/CopyCore/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "09f24667467c5ab460cd31c7e1845c760fc02ca2ebe0b3d769c8064d4351ba2d", + "originHash" : "f33f996e4954e262b1ab1dee3bd9c3637a3f988641ba7bb07afa3cffc03c2f22", "pins" : [ { "identity" : "grdb.swift", @@ -9,6 +9,24 @@ "revision" : "b83108d10f42680d78f23fe4d4d80fc88dab3212", "version" : "7.11.1" } + }, + { + "identity" : "keyboardshortcuts", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sindresorhus/KeyboardShortcuts", + "state" : { + "revision" : "49c3fc04ea827f816df67843bfcc57286b47ff06", + "version" : "3.0.1" + } + }, + { + "identity" : "sparkle", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sparkle-project/Sparkle", + "state" : { + "revision" : "b6496a74a087257ef5e6da1c5b29a447a60f5bd7", + "version" : "2.9.4" + } } ], "version" : 3