6uJM`Th(G|_qMX{oQv+W$UDsq#Lv#Tr3r#radQbEM)tGYtravt8^
z>Pum%cK%78_-0#s`(c?bh7pXE_mQ$&-wRh3i7OMrynSONx~YpNxB6qo
zh5njA`iZX-d(q>L=1aZVLHyBq<-|wUB2UeIqL~-&jZKARil@FjdgX*h{8yB|?8#vK
zJcFdB_xppVIlM!GEDHy(_I9GbY}WJWBxysY!X_UO)bD5ea2uSZ$b{FeE3>6qfF8h(
z`mc&*rKyjOSSbJEVmqZI5AAKA$Znrdf2CCuTDJ!P&8!Fp2cN;;T8(YOzI^)ei_JgV
zBcv}=w#NJ+q$m#oTEPy<a#KuAI61i143QPTrMWR?i9=F7u?7NPCnnkfML=1Fw?t7i^j-LL;&A|E
zdECn=iI@|UIJB0cWBKiP`Pq)^2?-thi=7TZBlER;NHsV|D+MDKKB^2e>htd+mgYb1
z=9&x1f`$2@-`l%DE%oa5hW}VhjZh7IGbiK;Q4k&7`G)NJtOmd^l%>JuWVGGIRb+$Y
zalXV!)y;LEQhZ+?@;b5a*u&uWqp8+p&YMIDi7{9Eg}-(|uEpyfs2L8PeX3+o9kUa!
zUAteq^z9G&(l_gdYS!ZxSqhQTaw+1g9`It8SYSXv1Z!iJehiAONz7K8zxV!_REZoI
z8VY%LH&LxRVyrvAIZyL7JMf8%wvxMUc*t9n@<7{SX6~q?BHqQ85|wKoSk=RGY=`P9
z|MI!d<=I#6<7OewgPYqUi1v%vKN=Trm>;5=(L>`3V>T;V3)hBaLdi<(aSS-x+
zs~%P=it`=r$)Ts9^LNJ%T(=vqoP0Zf*L@OKnyqlOfiUJ?cq*CmO|0Df;nDgZ54^9$
zciO2k?=eH6t8c)P!{$JPcU^zFyrX6wE?J-^mZqs4tL$%waEPm%U-a+-w$;L#V$*7lLFqiI>8B{%(AbPC6`ATJ`l@b<}2
z^jIic1&?1H$pQ5y%&(`wIPrb;(obFQhA+|c7v2mib3D+d?2wQ+geF7qd2|2FWS$)t
zy5r=FA|3ZR97-vk(?i}Y-+Lj_xqyOmRa@)tj0gw?EqeQTQJsR=ID;4E6&2Ouac47o
ze6cWF(iW@DUEV~g8(}Z-4Dl#h2I(C#Q9ajf$XZif{baM~ewfQzBIS~;WMR{~1adHn
zlH`Ip&eax!QT_j|2BLI<}@RGSnWe=cP~5dA$kDS^ZStcr;JcQTCXilKCY3Gk@i5X(>MwDO<=#S
zL=3XAfz!3Z^-MXfp$#qgK=1Oi=Y*m^0^P5eJ8Jvq!#uO(rNUgY6IfQP#CiS#Z+KA=
zoOx`H!le_wxV9SoAt9Eh7Kj~NlaD3)r@f$*#i8#jJQrq7SZ`;EcwoMha5b&%f|!Tf
zj-RJQY~1?CTa?)Ak9P8P6(~?UFxI(Ja&Koe{*CRDu;zI!3zW)du}UK!
z4w2OvL#5lAz86n{2m@xB^R$wKcXP>JcPJfLivBtYdM+}9gmg@#4lRe)F%qvhF5Dz}
zHMZyGHR;PjAi~k{jN6*#!(sqhcdb*ncOUB-S|LE>12=7uNE>{}_dH%Qs^=)PgMz+m
zK{yx6s2sJ5j>a~yHfB<*$`L!n)KVkqH#pZ@@wsmGq?1S2S@wcRgX27^ueT@Ohi!U{__>qVLfg
zsl$5OqWO>!X9t@1b_qNp#0^zqg3e70XQUz=24am18k5BTZ<%1YeM%!KKeWqwJqNd>riuRJD)?Tre^G2)N=jGvom
zwkD5G4`z1K0-3(DUvfC2CSx~l?D7}Ds5b*&6~+=Nhor6>Y}{3eI`m^5_u~dC8k>fe
zF}QcezPCwUF^q^*Qc3062UA;{d=#TVVp+Gpg6`9PVSlaBLEe;`%F7dFv$M2Lb?iC^
z=r+BF=wK!eR`uZfKG#LQ!dD)R%YCo0ox}2v*AP~+jNwqCMPqoHq_4sC{?o;vxF|2V
zj&_XR)&W7icI)Sr=M{{IBAP|U?*=~A4Nqi*ZfWF1ZL=!09|-@}OF1k#pl8b&V2g8d
z$>xxMz%l@GE%cz?8Z5S0w^zp?t|)~ZfgBUbX8ukldF_hIOfkq;Om3TN$+ozrt7mB-
zQV!5_12*I3<0GveI5lWxP8WjC=9*-p)qeb!4A^`s>N}$UM)ge0EmCB=RF`5vFgcjq
z|9W^>y{o_f3}Om)pz5L<)o_R>!1j(J(Ft>#onS!e-H-dMrnr~u#|ZJ$sJqQ3UBu0~@ug<388%Y|1?IR}yubL-v@BglIP#tfeSFU-T=)11Of&>R19IbRUvDa0(%i
z;ry<+JZ;}9(#oYMhwN|b!l#N+Si4The$K#=1&=2I`JV(r#d*o$Hlv=aQIH&ZS(u+m
z+KmWq5%)mJ)Nxkn=z%v+2Z9av+=?^UI{)LxW^j`Ad{V^0Rn%woCW?|ZktIDZnKI9z
zs5#oO5sVzlZXYCm4OwMP3!ECV!kZscUs4D#7QbjV0mA_pU2{I
z>xJ1u4D-8>OQ!CP!fY|){s`4e+hvpleM!{5*5!JRhS)-UYHef
zo`>aPTr$GW;{Wg62NvT%eMZiCcg40J%wmy}DS7x}A*5Zm10$21{7fFD7s4P6yw<*gdqZ(_9eOm3%y022Un?;`eBjF;ogN}emW5G!hlT{T%<`_xfn_qbfuEgC)aBqpX4l-x`$Lk?!jzGSxSB4;F4ugy?HialCFT2+!35x^yaN>_1JzsuyYtutp5JrI}UOL;)pZ$WjxuJC8
z?W4Po9iCn?$cUHNjqNbp)pSU!yOuF9Z3xu=6mPj%90oVO>EY3#j`PCs^R|7f8PBQD
zodWjEP8!=!^JwozE>f;gtGYt7F*o+RQ}H2mLlIrfH5LtNINu*cSxQ82fekOhb6L2E
z>ANHU4n&r6ZEH;IJ|5WK%uu2f8%SjM?P^&Hyxs)J@b>x&`>rMb@i)H#h$g_Y27VG&B(`&vKihyntpYt|*gqhEseH`pRBdhA
zd)KlvDciXzK7bQi=GwsbTDZ#(nP$2CKJesl;sEFA%Sc61SU0|s8)7MH5RX9WJTpIg
z)5CkspRwkNa8tyS+fJ*tOM*Tfofk+=sbe$|D|VHiE3F8n$5X404J~WX20xU%U)Agx
zIJ?y0VWO5jJTN@mPy@_UF1bxhKXMzy-)U%zUfFu(lBOqD5O8o#La&Z)g)&Vch;Hvg
z^}L$v7Py^-xk%u|>7#t{&WmC7NY3}Gi2*5Ib@R7*dv&Zqj<^oQhakHfm9E@91j{``
z14HPF;DL1E(eB)&(Z%cltj5H|q_fLRSKw6llcU`3Rjwk%NBjR@qpA_NxxH0>2u!h$B78}QXfdA
z7)ga$>W^sjx
zghLij#W)b&B*d`ThKsNf5R9#C2F`9?XE?0+Ob#k-t?d)_5G>)sAAK7$@tiih$#`rB
zH(m)P4-zT&7Ci!x^5$F0ckHx8-p3(!a%Hd0c;1iK4c}~Y$>M7A(PRIpt;LxML!{Vr4JFR=+;*4!!zyl$I%a17Uw|r>6b?cTv_Z={1
zT#YkO=046N1WD?^4NClksdXUs0jbIcp|OYpHd+I%yxl;QyFh?`==1R}J(Q;!74L{@
z?U)#jYjz3*487+M&fzvQGyOYomqL2^yn!u<9N2=}dwthS`~JSZz+w0j1qibi#dc4?
zdfdZg>RzBt;tUP4Dd(Jt!_41%p~d{rVJw*`{p`(?T9d~goj_%eJ-qr#w168ciRQ(A
zV{yEP1r>2AJV4jGVhxmod*k8?1VMbh6eL0@Wc>7{4KiTo@eskmon&9go#N+IWh;5R
z-)DUwV_WlpO-7@Hxs#fXKlW)(QLp{tz1GU=}OT*DJg`Oy#BS|XzW%$LmK
z%{4t}kVf9rGtqpynBrhqvvvKPqV+XsxEtN7zoxcm9b3*lFkJfTu{rc@s~qxQyb}4_
zG;e5V*3ecIpw+qO8qCiJ1@R+A=$x!#5yOMwFEbf-0k5prhUpXGcHpYX69hc4)YWCh
zm>vLluwEC*ATf(XexGkxr5&y~_Ub+J`cME^yUoH~fV9sqUhSJp2%(n(dl-&b7~1QX
zb~jc}02F#D)*S8iFK0Hy*nD*n7$4-HjA5`NIJUyJTN?LXqV~IHugXO!A`lmF
z0vN64tQ$ncU#k#1S?u(P-vzSLa~AA^QxSP7;=S@cM*Rmn;>~Kx@^E*f6;g*t=zrM>
zN-(HY=8IuNdPJ14kbPMmjx7(b{k&&Dcqq`wj#Mze#Rwb7QNY^6$D1-Ii({{4C{ygT
zMtGmqU^(rBM^lEE=L*$VS|U{*Qi%J
z&KIi97t`-lG=ZT*P8jqBAhErM3*J(g8s*rkkdJ2dxqVfD%J9bd|EpAtM)iIYJeFrzYcTD+x
zsH^5NYH@nW66D(X5$Egu6hE-luM4CKpvoqlxbyH$d)9gs9M6F^(4uW}Uct31fV<*E
zM}7Ff18vzFDp6PUcE%CoeFbQ<-F-_5Yo;KMbf*2-ZPa!)2i@%~4eJ3NBD!D|JaYC)(Ly^o@+ax^
zHB8$ix;u(X66M%t2VHbyB6y8%%t&K9ghPb=wMA@#0`FEDkzLcyr1>3$sBCk111Eet
zOQIO67n7aAW5TQ}YjcEenEZ0|r(&Dp?Xh=a*o5YGRh0Fv$(Eg4hu#PTWeCWW&9o%!
z6{5qu6%}5;>;wM1FWC|!
zca7~#mE-1)l8xdt#>{FENha}U4OaTMKF8anozatxAJ7gIJkD1>vs1YKd4ewA
z5lQ*ddDTy92X@BHh3yqujHQ(jI}AI$#1P-a4}L9pP_yE+ln0xOc1J-t(g_1&oM#
z(o_(+Fs*;QmQnV(RsF4)I%eTdBgj<>dWDhZbe}YfL(~XFMsJOrV-H8L_Q%IJ}Ykcc;-(`lH
zTv?P0zqctj)AJh~p9inj=}yh6F1~IQ(tk&r-X}D+{JQileZGR8O$T?;T6M(y%5b2_
zZ>ldM8U*8}*FG%h5q}>@xRlyHfr@g5{LzW&{atF=he3u1CyT}nzoK(WLR&thiO2DQ
zezFUK!X)!%sd#Mt_8&uZQT%q6r#?K|DZ*?LL5rL#3!8#BAE$;6Gv##PQ(3#F>gYaH
zo$aMa|;m@LuvVx3h_OGhvcdgDMM`Cl`N+=ze
zuImGA#ys|N*0l^?nyoz53X$g1e#PI0jbdthWOUF%oG(5tHvEsJKCI9=CzQPm%~3r-?hB-Uh&>M6dG@MFW%egcWPCXOH2Ep+Nfll?)E>*
zB`3mH=2@s!9$`JX0>^78{?9sN_
z1FKNqs=*$l;`SL9;G^LXW0kY;3D2BUZYuty{K=D*waIseUVC-7BVBu_g|1qyLXC#m
z#|yheg9KmFK8gly&1zKH{g7ZYW7H|des!y}SUk&?4=i*{f@8Aqb7E)iy+7WLW{elO
zzX@U+I(a}Sv2|=_W~Mmz;V|8~@?G;sA909uqnbQ65t}+B$W)<#t#(g0*LpIm8hGl|
z>seLZGq+yK8Cw{*e_f6!z9&)OE^+Uk1T9$dKyDVTN+K>-u=UZAmygqEi@k5;{Ld+t
zjHkbR9JX~J#_=m;Ap@&(^kjfa_|lv^>$Mk%ZOpYQC7JJGBry
zF4*9`i)%S_VEp{P)`Qse;4H`zX-usGn`LlV1n&Chf|BvzbA69W#%oJ1K0~MK{D)WMOaubFbr`}bJey$Cv
z)u9G^rV}TY)ha!2f8MX5eCp+w&(liT?}x|1_4;Y@U8PCG(})JCfSPl^sR}Blrl!80
zob(Kt{-g~SgtD9674&7Aw%JNIW3^eD<2{6Y4LI_5LyMXthAE^HU#dT~>UykG7VVSe
ztFdayKL2CzX3D*LEj_j4)y3j*Ri5u)6Y$%;tpA#EkJ0oE)6pP3(}Lgj6~@QM_oUx)
z{MY
zq5^jV|8s?o{MCdr1Ix?HCG_QZ_!F?NHRQ}K`Fyz)0;TeuXTy812ETKCrQ-vYp36sR
zD-Yb$mxJ{FYDpAQ=Pb3W`xc-3Jo4dYB$n;!*0l`vn$$PyA(Kl>)qmF4Uyl_BXlQ8-
z4i56fG<}RwI1;hP#M2ti_|K{;kV-jYG$#UT{2PX|S*z9x>r_nMntZ-_B=g>>Ve3;T
z*JPASb2HTK)?6|g)?7(LOaW(S$M#G+=YOrug)bub|Ihym5LoG3*{YLmH&t-1D-7L_
zzV=D2k`oT_{jqo0;rkR3Q#WrvZ*3QMKW|@qbAmnkpS_l&pJOQ2+t=OI-OJI#)X~ok
ztBkdka)Dx7DH6fiR>}wMMOg1;DDDt!O=w;?;#!32kxnhrCeF;9k
z1QNl^&(Y64fS}{+=t(g0cE&0nl{tbX`8oRf8GHM=dwW^AyAZrLN(o*r|EtWM^sgdg
ze?J0Nnds<2A{@cGlPrDRJ^xkvPmQyOyN|vX(c8+=!ynpXLv691ny?Up?`d1Cvb3a}
z5>8G|R$fs~5r>zPSCBpOPyLzodU`*}-r=XxHCy&-zySw(#J*B_%u#CnpP#ktItIfSp5O{l`4gUE|`1_Nd4y^X?4_5#F
z@OM9LB9Y*{KK%dm{?D@YG5)h?eWd>^((?{6CHRtjU@$n4f2Jh({#R)@9G;)I7vW#v
za{?rC^YbE*NYG728r=pV$j6)H?~9JJQQ++D<>%{2LKnJGKyvi+_x*e48%1vZ|In;D;?nYzFU>|gA(&%_T-o5~E
zcfXBO{@1u06Y0A^!*$qhd^REk5Ip`V3UVj@jbG?^Fy}h-{%(X8_<8#~yRF}tzdx*B
z5BT539Vb7;MZrg`d<_3yO{iQKnQwsFZmtf%@0;Ea~9o53w(Sx9&|IY}%
zzvu_tt0P!Pm-GH4KTn`$|2l9_OIP88~uTAK|-LN<^KVsD=UJJ!dH&Nh(b>x;pdjO-Cx^&%ieK$hi?
zm>YaqF9K2p6vh1D=taO^2bASo2n0j{kfEmrmp%j>6_91sBjy21dJ&L5U~F*oA%Fk^
zjtS7Ab?~wbhqpd&AP^7*peS^+F+8~Sncuh&kSf4WMg1~F38D`H#{^(#1N4VC;{2jT
zKzabb6d6Fu(1)cb;HUswh{6zz0;ZQN0#XGOMV5`HSA;G_1{m7B5|AbUkD}(nwxr|R
zmQ^4EA^`NuZ3zcGm4F4St_esH5EkGNU<~B}^7jVkPr&Yg;Z%OJIz&JLZc>BF8-
z1oOPE$9mub2B}vpMg&9zkYUUJ5dy3w1ZeRQkRHI7pgyV?UW%G#t_)vZhzQsjpozx-
z?IXq(Bmv0lODiD(y8~pRL87KnipRCBCs;vZa*}cEB87l-0RG05E;iEuB`CmdSgv}E
zML+}qn@s9jHWeDeQHZqp7D?^_Whw1ij0B_#(2b?V7yvi}XyO;=K>~IGXnGV|M7(he0LFM?E+k-gfHHIdO@*nY?F9&E@i!+F
zfaPe_o``^G0L@RdGa-F+0mCJUZA(|)D<@!QfD)boAgwRK;f=_`Z7Kng0BkB+5=h(y
zbP19eAAcku1%MGyiv6++%P`!Eb^-n?Q3S*e=nt_pw6}k?_dj+11VjM9R-!v3HlX-X
z44H6>LT^ziz<-R<6KumuKpFu12%=H4?0l{&xg8~6X2IqIB_+qs=b?g>$5j$}CkRLb
z(5FcH%OfP+>7+L}14o1qwgIq&KtL*h;S_is16;2|8<5S;pMZ}7x(P51
z`DLfuYJhnH0qFu{(>V;V#mE3-gq8mX2>3W4Oz*d!{b@*3Uaxb27tWu6odI^cA}m4s
zH`3pJBP6Wa4HFOn03|pMkk)BH`0$E=6aaD?!ry$vQ5n`YARz!ffB<0V@g)H%0PHa{
ze&WOcNT8w1l%>}K)UPFyx%pQmE7%izsqrH?UpV9M+1P+P2Z)wEinO60sQ2nfH#*O0Z{>(-{>w!
zdW?X-0I&_IUa {
+export default () =>
+ {
name: "debug",
description: Strings.COMMAND_DEBUG_DESC,
options: [
- {
- name: "ephemeral",
- type: ApplicationCommandOptionType.BOOLEAN,
- description: Strings.COMMAND_DEBUG_OPT_EPHEMERALLY,
- }
+ {
+ name: "ephemeral",
+ type: ApplicationCommandOptionType.BOOLEAN,
+ description: Strings.COMMAND_DEBUG_OPT_EPHEMERALLY,
+ },
],
execute([ephemeral], ctx) {
- const info = getDebugInfo();
- const content = [
- "**Kettu Debug Info**",
- `> Kettu: ${info.bunny.version} (${info.bunny.loader.name} ${info.bunny.loader.version})`,
- `> Discord: ${info.discord.version} (${info.discord.build})`,
- `> React: ${info.react.version} (RN ${info.react.nativeVersion})`,
- `> Hermes: ${info.hermes.version} (bcv${info.hermes.bytecodeVersion})`,
- `> System: ${info.os.name} ${info.os.version} ${info.os.sdk ? `(SDK ${info.os.sdk})` : ""}`.trimEnd(),
- `> Device: ${info.device.model} (${info.device.codename})`,
- ].join("\n");
+ const info = getDebugInfo();
+ const content = [
+ "**ShiggyCord Debug Info**",
+ `> ShiggyCord: ${info.bunny.version} (${info.bunny.loader.name} ${info.bunny.loader.version})`,
+ `> Discord: ${info.discord.version} (${info.discord.build})`,
+ `> React: ${info.react.version} (RN ${info.react.nativeVersion})`,
+ `> Hermes: ${info.hermes.version} (bcv${info.hermes.bytecodeVersion})`,
+ `> System: ${info.os.name} ${info.os.version} ${info.os.sdk ? `(SDK ${info.os.sdk})` : ""}`.trimEnd(),
+ `> Device: ${info.device.model}`,
+ ].join("\n");
- if (ephemeral?.value) {
- messageUtil.sendBotMessage(ctx.channel.id, content);
- } else {
- const fixNonce = Date.now().toString();
- messageUtil.sendMessage(ctx.channel.id, { content }, void 0, {nonce:fixNonce});
- }
- }
-};
+ if (ephemeral?.value) {
+ messageUtil.sendBotMessage(ctx.channel.id, content);
+ } else {
+ const fixNonce = Date.now().toString();
+ messageUtil.sendMessage(ctx.channel.id, { content }, void 0, {
+ nonce: fixNonce,
+ });
+ }
+ },
+ };
diff --git a/src/core/i18n/default.json b/src/core/i18n/default.json
index afd4443..f13e3b1 100644
--- a/src/core/i18n/default.json
+++ b/src/core/i18n/default.json
@@ -1,121 +1,121 @@
{
- "ABOUT": "About",
- "ACTIONS": "Actions",
- "ARE_YOU_SURE_TO_CLEAR_DATA": "Are you sure you wish to clear the data of {name}?",
- "ARE_YOU_SURE_TO_DELETE_PLUGIN": "Are you sure you wish to delete {name}? This will clear all of the plugin's data.",
- "ARE_YOU_SURE_TO_DELETE_THEME": "Are you sure you wish to delete {name}?",
- "ASSET_BROWSER": "Asset Browser",
- "BRAND": "Brand",
- "PUPU": "Kettu",
- "PUPU_URL": "Kettu URL",
- "BROWSER": "Browser",
- "BYTECODE": "Bytecode",
- "CANCEL": "Cancel",
- "CLEAR": "Clear",
- "CLEAR_BUNDLE": "Clear JS Bundle",
- "CLEAR_BUNDLE_DESC": "Clear the cached bundle. This will force a re-download of the bundle next app launch.",
- "CLEAR_DATA": "Clear data",
- "CLEAR_DATA_FAILED": "Failed to clear data for {name}",
- "CLEAR_DATA_SUCCESSFUL": "Cleared data for {name}",
- "CODEBERG": "Codeberg",
- "CODENAME": "Codename",
- "COMMAND_DEBUG_DESC": "Send Kettu debug info.",
- "COMMAND_DEBUG_OPT_EPHEMERALLY": "Send debug info ephemerally.",
- "COMMAND_EVAL_DESC": "Evaluate JavaScript code.",
- "COMMAND_EVAL_OPT_ASYNC": "Whether to support 'await' in code. Must explicitly return for result (default: false)",
- "COMMAND_EVAL_OPT_CODE": "The code to evaluate.",
- "COMMAND_PLUGINS_DESC": "Send list of installed plugins.",
- "COMMAND_PLUGINS_OPT_EPHEMERALLY": "Send plugins list ephemerally.",
- "COMPONENT": "Component",
- "CONFIRMATION_LINK_IS_A_TYPE": "This link is a **{urlType, select, plugin {Plugin} theme {Theme} other {Add-on}}**, would you like to install it?",
- "CONNECT_TO_DEBUG_WEBSOCKET": "Connect to debug WebSocket",
- "CONNECT_TO_REACT_DEVTOOLS": "Connect to React DevTools",
- "CONTINUE": "Continue",
- "COPIED_TO_CLIPBOARD": "Copied to clipboard",
- "COPY_URL": "Copy URL",
- "DEBUG": "Debug",
- "DEBUGGER_URL": "Debugger URL",
- "DELETE": "Delete",
- "DESC_EXTRACT_FONTS_FROM_THEME": "Looks out for \"fonts\" field in your currently applied theme and install it.",
- "DEVELOPER": "Developer",
- "DEVELOPER_SETTINGS": "Developer Settings",
- "DISABLE_THEME": "Disable Theme",
- "DISABLE_UPDATES": "Disable Updates",
- "DISCORD_SERVER": "Discord Server",
- "DONE": "Done",
- "ENABLE_EVAL_COMMAND": "Enable /eval command",
- "ENABLE_EVAL_COMMAND_DESC": "Evaluate JavaScript directly from a command. Be cautious when using this command as it may pose a security risk. Make sure to know what you are doing.",
- "ENABLE_UPDATES": "Enable Updates",
- "ERROR_BOUNDARY_TOOLS_LABEL": "ErrorBoundary Tools",
- "EXTRACT": "Extract",
- "FONT_NAME": "Font Name",
- "FONTS": "Fonts",
- "GENERAL": "General",
- "GITHUB": "GitHub",
- "HOLD_UP": "Hold Up",
- "INFO": "Info",
- "INSTALL": "Install",
- "INSTALL_ADDON": "Install an add-on",
- "INSTALL_FONT": "Install a font",
- "INSTALL_PLUGIN": "Install a plugin",
- "INSTALL_REACT_DEVTOOLS": "Install React DevTools",
- "INSTALL_THEME": "Install a theme",
- "LABEL_EXTRACT_FONTS_FROM_THEME": "Extract font from theme",
- "LINKS": "Links",
- "LOAD_FROM_CUSTOM_URL": "Load from custom URL",
- "LOAD_FROM_CUSTOM_URL_DEC": "Load Kettu from a custom endpoint.",
- "LOAD_REACT_DEVTOOLS": "Load React DevTools",
- "LOADER": "Loader",
- "MACHINE_ID": "Machine ID",
- "MANUFACTURER": "Manufacturer",
- "MESSAGE": "Message",
- "MISCELLANEOUS": "Miscellaneous",
- "MODAL_RELOAD_REQUIRED": "Reload app?",
- "MODAL_RELOAD_REQUIRED_DESC": "A reload is required to see the changes. Do you want to reload now?",
- "MODAL_THEME_REFETCHED": "Theme refetched",
- "MODAL_THEME_REFETCHED_DESC": "A reload is required to see the changes. Do you want to reload now?",
- "MODAL_UNPROXIED_PLUGIN_DESC": "The plugin you are trying to install has not been proxied/verified by staff. Are you sure you want to continue?",
- "MODAL_UNPROXIED_PLUGIN_HEADER": "Unproxied Plugin",
- "MODEL": "Model",
- "OPEN_IN_BROWSER": "Open in Browser",
- "OPERATING_SYSTEM": "Operating System",
- "OVERFLOW_PLUGIN_SETTINGS": "Plugin Settings",
- "PLATFORM": "Platform",
- "PLUGIN_REFETCH_FAILED": "Failed to refetch plugin",
- "PLUGIN_REFETCH_SUCCESSFUL": "Successfully refetched plugin",
- "PLUGINS": "Plugins",
- "REFETCH": "Refetch",
- "RELOAD": "Reload",
- "RELOAD_DISCORD": "Reload Discord",
- "RELOAD_IN_NORMAL_MODE": "Reload in Normal Mode",
- "RELOAD_IN_NORMAL_MODE_DESC": "Safe mode currently enabled, tap to reload in normal mode",
- "RELOAD_IN_SAFE_MODE": "Reload in Safe Mode",
- "RELOAD_IN_SAFE_MODE_DESC": "Tap to reload Discord without loading addons",
- "REMOVE": "Remove",
- "RESTART_REQUIRED_TO_TAKE_EFFECT": "Restart is required to take effect",
- "RETRY": "Retry",
- "RETRY_RENDER": "Retry Render",
- "SAFE_MODE": "Safe Mode",
- "SAFE_MODE_NOTICE_FONTS": "You are in safe mode, meaning fonts have been temporarily disabled. {enabled, select, true {If a font appears to be causing the issue, you can press below to disable it persistently.} other {}}",
- "SAFE_MODE_NOTICE_PLUGINS": "You are in safe mode, so plugins cannot be loaded. Disable any misbehaving plugins, then return to Normal Mode from the General settings page.",
- "SAFE_MODE_NOTICE_THEMES": "You are in safe mode, meaning themes have been temporarily disabled. {enabled, select, true {If a theme appears to be causing the issue, you can press below to disable it persistently.} other {}}",
- "SEARCH": "Search",
- "SEPARATOR": ", ",
- "SETTINGS_ACTIVATE_DISCORD_EXPERIMENTS": "Activate Discord Experiments",
- "SETTINGS_ACTIVATE_DISCORD_EXPERIMENTS_DESC": "Warning: Messing with this feature may lead to account termination. I heavily discourage using this and am not responsible for anything that happens if you use it",
- "STACK_TRACE": "Stack Trace",
- "SUCCESSFULLY_INSTALLED": "Successfully installed",
- "THEME_EXTRACTOR_DESC": "This pack overrides the following: {fonts}",
- "THEME_REFETCH_FAILED": "Failed to refetch theme",
- "THEME_REFETCH_SUCCESSFUL": "Successfully refetched theme",
- "THEMES": "Themes",
- "THEMES_RELOAD_FOR_CHANGES": "Reload the app to fully apply changes",
- "TOASTS_INSTALLED_PLUGIN": "Installed plugin",
- "TOASTS_PLUGIN_UPDATE": "{update, select, true {Enabled} other {Disabled}} updates for {name}",
- "UH_OH": "Uh Oh",
- "UNINSTALL": "Uninstall",
- "UNINSTALL_TITLE": "Uninstall {title}",
- "URL_PLACEHOLDER": "https://codeberg.org/cocobo1/Kettu",
- "VERSION": "Version",
- "VERSIONS": "Versions"
-}
\ No newline at end of file
+ "ABOUT": "About",
+ "ACTIONS": "Actions",
+ "ARE_YOU_SURE_TO_CLEAR_DATA": "Are you sure you wish to clear the data of {name}?",
+ "ARE_YOU_SURE_TO_DELETE_PLUGIN": "Are you sure you wish to delete {name}? This will clear all of the plugin's data.",
+ "ARE_YOU_SURE_TO_DELETE_THEME": "Are you sure you wish to delete {name}?",
+ "ASSET_BROWSER": "Asset Browser",
+ "BRAND": "Brand",
+ "PUPU": "ShiggyCord",
+ "PUPU_URL": "ShiggyCord URL",
+ "BROWSER": "Browser",
+ "BYTECODE": "Bytecode",
+ "CANCEL": "Cancel",
+ "CLEAR": "Clear",
+ "CLEAR_BUNDLE": "Clear JS Bundle",
+ "CLEAR_BUNDLE_DESC": "Clear the cached bundle. This will force a re-download of the bundle next app launch.",
+ "CLEAR_DATA": "Clear data",
+ "CLEAR_DATA_FAILED": "Failed to clear data for {name}",
+ "CLEAR_DATA_SUCCESSFUL": "Cleared data for {name}",
+ "CODEBERG": "Codeberg",
+ "CODENAME": "Codename",
+ "COMMAND_DEBUG_DESC": "Send ShiggyCord debug info.",
+ "COMMAND_DEBUG_OPT_EPHEMERALLY": "Send debug info ephemerally.",
+ "COMMAND_EVAL_DESC": "Evaluate JavaScript code.",
+ "COMMAND_EVAL_OPT_ASYNC": "Whether to support 'await' in code. Must explicitly return for result (default: false)",
+ "COMMAND_EVAL_OPT_CODE": "The code to evaluate.",
+ "COMMAND_PLUGINS_DESC": "Send list of installed plugins.",
+ "COMMAND_PLUGINS_OPT_EPHEMERALLY": "Send plugins list ephemerally.",
+ "COMPONENT": "Component",
+ "CONFIRMATION_LINK_IS_A_TYPE": "This link is a **{urlType, select, plugin {Plugin} theme {Theme} other {Add-on}}**, would you like to install it?",
+ "CONNECT_TO_DEBUG_WEBSOCKET": "Connect to debug WebSocket",
+ "CONNECT_TO_REACT_DEVTOOLS": "Connect to React DevTools",
+ "CONTINUE": "Continue",
+ "COPIED_TO_CLIPBOARD": "Copied to clipboard",
+ "COPY_URL": "Copy URL",
+ "DEBUG": "Debug",
+ "DEBUGGER_URL": "Debugger URL",
+ "DELETE": "Delete",
+ "DESC_EXTRACT_FONTS_FROM_THEME": "Looks out for \"fonts\" field in your currently applied theme and install it.",
+ "DEVELOPER": "Developer",
+ "DEVELOPER_SETTINGS": "Developer Settings",
+ "DISABLE_THEME": "Disable Theme",
+ "DISABLE_UPDATES": "Disable Updates",
+ "DISCORD_SERVER": "Discord Server",
+ "DONE": "Done",
+ "ENABLE_EVAL_COMMAND": "Enable /eval command",
+ "ENABLE_EVAL_COMMAND_DESC": "Evaluate JavaScript directly from a command. Be cautious when using this command as it may pose a security risk. Make sure to know what you are doing.",
+ "ENABLE_UPDATES": "Enable Updates",
+ "ERROR_BOUNDARY_TOOLS_LABEL": "ErrorBoundary Tools",
+ "EXTRACT": "Extract",
+ "FONT_NAME": "Font Name",
+ "FONTS": "Fonts",
+ "GENERAL": "General",
+ "GITHUB": "GitHub",
+ "HOLD_UP": "Hold Up",
+ "INFO": "Info",
+ "INSTALL": "Install",
+ "INSTALL_ADDON": "Install an add-on",
+ "INSTALL_FONT": "Install a font",
+ "INSTALL_PLUGIN": "Install a plugin",
+ "INSTALL_REACT_DEVTOOLS": "Install React DevTools",
+ "INSTALL_THEME": "Install a theme",
+ "LABEL_EXTRACT_FONTS_FROM_THEME": "Extract font from theme",
+ "LINKS": "Links",
+ "LOAD_FROM_CUSTOM_URL": "Load from custom URL",
+ "LOAD_FROM_CUSTOM_URL_DEC": "Load ShiggyCord from a custom endpoint.",
+ "LOAD_REACT_DEVTOOLS": "Load React DevTools",
+ "LOADER": "Loader",
+ "MACHINE_ID": "Machine ID",
+ "MANUFACTURER": "Manufacturer",
+ "MESSAGE": "Message",
+ "MISCELLANEOUS": "Miscellaneous",
+ "MODAL_RELOAD_REQUIRED": "Reload app?",
+ "MODAL_RELOAD_REQUIRED_DESC": "A reload is required to see the changes. Do you want to reload now?",
+ "MODAL_THEME_REFETCHED": "Theme refetched",
+ "MODAL_THEME_REFETCHED_DESC": "A reload is required to see the changes. Do you want to reload now?",
+ "MODAL_UNPROXIED_PLUGIN_DESC": "The plugin you are trying to install has not been proxied/verified by staff. Are you sure you want to continue?",
+ "MODAL_UNPROXIED_PLUGIN_HEADER": "Unproxied Plugin",
+ "MODEL": "Model",
+ "OPEN_IN_BROWSER": "Open in Browser",
+ "OPERATING_SYSTEM": "Operating System",
+ "OVERFLOW_PLUGIN_SETTINGS": "Plugin Settings",
+ "PLATFORM": "Platform",
+ "PLUGIN_REFETCH_FAILED": "Failed to refetch plugin",
+ "PLUGIN_REFETCH_SUCCESSFUL": "Successfully refetched plugin",
+ "PLUGINS": "Plugins",
+ "REFETCH": "Refetch",
+ "RELOAD": "Reload",
+ "RELOAD_DISCORD": "Reshiggy your Discord",
+ "RELOAD_IN_NORMAL_MODE": "Reload in Normal Mode",
+ "RELOAD_IN_NORMAL_MODE_DESC": "Safe mode currently enabled, tap to reload in normal mode",
+ "RELOAD_IN_SAFE_MODE": "Reload in Safe Mode",
+ "RELOAD_IN_SAFE_MODE_DESC": "Tap to reload Discord without loading addons",
+ "REMOVE": "Remove",
+ "RESTART_REQUIRED_TO_TAKE_EFFECT": "Restart is required to take effect",
+ "RETRY": "Retry",
+ "RETRY_RENDER": "Retry Render",
+ "SAFE_MODE": "Safe Mode",
+ "SAFE_MODE_NOTICE_FONTS": "You are in safe mode, meaning fonts have been temporarily disabled. {enabled, select, true {If a font appears to be causing the issue, you can press below to disable it persistently.} other {}}",
+ "SAFE_MODE_NOTICE_PLUGINS": "You are in safe mode, so plugins cannot be loaded. Disable any misbehaving plugins, then return to Normal Mode from the General settings page.",
+ "SAFE_MODE_NOTICE_THEMES": "You are in safe mode, meaning themes have been temporarily disabled. {enabled, select, true {If a theme appears to be causing the issue, you can press below to disable it persistently.} other {}}",
+ "SEARCH": "Search",
+ "SEPARATOR": ", ",
+ "SETTINGS_ACTIVATE_DISCORD_EXPERIMENTS": "Activate Discord Experiments",
+ "SETTINGS_ACTIVATE_DISCORD_EXPERIMENTS_DESC": "Warning: Messing with this feature may lead to account termination. I heavily discourage using this and am not responsible for anything that happens if you use it",
+ "STACK_TRACE": "Stack Trace",
+ "SUCCESSFULLY_INSTALLED": "Successfully installed",
+ "THEME_EXTRACTOR_DESC": "This pack overrides the following: {fonts}",
+ "THEME_REFETCH_FAILED": "Failed to refetch theme",
+ "THEME_REFETCH_SUCCESSFUL": "Successfully refetched theme",
+ "THEMES": "Themes",
+ "THEMES_RELOAD_FOR_CHANGES": "Reload the app to fully apply changes",
+ "TOASTS_INSTALLED_PLUGIN": "Installed plugin",
+ "TOASTS_PLUGIN_UPDATE": "{update, select, true {Enabled} other {Disabled}} updates for {name}",
+ "UH_OH": "Uh Oh",
+ "UNINSTALL": "Uninstall",
+ "UNINSTALL_TITLE": "Uninstall {title}",
+ "URL_PLACEHOLDER": "https://github.com/kmmiio99o/ShiggyCord",
+ "VERSION": "Version",
+ "VERSIONS": "Versions"
+}
diff --git a/src/core/i18n/index.ts b/src/core/i18n/index.ts
index efeea3b..910a0ea 100644
--- a/src/core/i18n/index.ts
+++ b/src/core/i18n/index.ts
@@ -4,7 +4,9 @@ import { PrimitiveType } from "intl-messageformat";
import langDefault from "./default.json";
-const IntlMessageFormat = findByNameLazy("MessageFormat") as typeof import("intl-messageformat").default;
+const IntlMessageFormat = findByNameLazy(
+ "MessageFormat",
+) as typeof import("intl-messageformat").default;
type I18nKey = keyof typeof langDefault;
@@ -14,55 +16,73 @@ let _lastSetLocale: string | null = null;
const _loadedLocale = new Set();
const _loadedStrings = {} as Record;
-export const Strings = new Proxy({}, {
+export const Strings = new Proxy(
+ {},
+ {
get: (_t, prop: keyof typeof langDefault) => {
- if (_currentLocale && _loadedStrings[_currentLocale]?.[prop]) {
- return _loadedStrings[_currentLocale]?.[prop];
- }
- return langDefault[prop];
- }
-}) as Record;
+ if (_currentLocale && _loadedStrings[_currentLocale]?.[prop]) {
+ return _loadedStrings[_currentLocale]?.[prop];
+ }
+ return langDefault[prop];
+ },
+ },
+) as Record;
export function initFetchI18nStrings() {
- const cb = ({ locale }: { locale: string; }) => {
- const languageMap = {
- "es-ES": "es",
- "es-419": "es_419",
- "zh-TW": "zh-Hant",
- "zh-CN": "zh-Hans",
- "pt-PT": "pt",
- "pt-BR": "pt_BR",
- "sv-SE": "sv"
- } as Record;
+ const cb = ({ locale }: { locale: string }) => {
+ const languageMap = {
+ "es-ES": "es",
+ "es-419": "es_419",
+ "zh-TW": "zh-Hant",
+ "zh-CN": "zh-Hans",
+ "pt-PT": "pt",
+ "pt-BR": "pt_BR",
+ "sv-SE": "sv",
+ } as Record;
- const resolvedLocale = _lastSetLocale = languageMap[locale] ?? locale;
+ const resolvedLocale = (_lastSetLocale = languageMap[locale] ?? locale);
- if (resolvedLocale.startsWith("en-")) {
- _currentLocale = null;
- return;
- }
+ if (resolvedLocale.startsWith("en-")) {
+ _currentLocale = null;
+ return;
+ }
- if (!_loadedLocale.has(resolvedLocale)) {
- _loadedLocale.add(resolvedLocale);
+ if (!_loadedLocale.has(resolvedLocale)) {
+ _loadedLocale.add(resolvedLocale);
- fetch(`https://raw.githubusercontent.com/C0C0B01/i18n/main/resources/${resolvedLocale}/kettu.json`)
- .then(r => r.json())
- .then(strings => _loadedStrings[resolvedLocale] = strings)
- .then(() => resolvedLocale === _lastSetLocale && (_currentLocale = resolvedLocale))
- .catch(e => console.error(`An error occured while fetching strings for ${resolvedLocale}: ${e}`));
- } else {
- _currentLocale = resolvedLocale;
- }
- };
+ fetch(
+ `https://raw.githubusercontent.com/C0C0B01/i18n/main/resources/${resolvedLocale}/shiggycord.json`,
+ )
+ .then((r) => r.json())
+ .then((strings) => (_loadedStrings[resolvedLocale] = strings))
+ .then(
+ () =>
+ resolvedLocale === _lastSetLocale &&
+ (_currentLocale = resolvedLocale),
+ )
+ .catch((e) =>
+ console.error(
+ `An error occured while fetching strings for ${resolvedLocale}: ${e}`,
+ ),
+ );
+ } else {
+ _currentLocale = resolvedLocale;
+ }
+ };
- FluxDispatcher.subscribe("I18N_LOAD_SUCCESS", cb);
- return () => FluxDispatcher.unsubscribe("I18N_LOAD_SUCCESS", cb);
+ FluxDispatcher.subscribe("I18N_LOAD_SUCCESS", cb);
+ return () => FluxDispatcher.unsubscribe("I18N_LOAD_SUCCESS", cb);
}
-type FormatStringRet = T extends PrimitiveType ? string : string | T | (string | T)[];
+type FormatStringRet = T extends PrimitiveType
+ ? string
+ : string | T | (string | T)[];
-export function formatString(key: I18nKey, val: Record): FormatStringRet {
- const str = Strings[key];
- // @ts-ignore
- return new IntlMessageFormat(str).format(val);
+export function formatString(
+ key: I18nKey,
+ val: Record,
+): FormatStringRet {
+ const str = Strings[key];
+ // @ts-ignore
+ return new IntlMessageFormat(str).format(val);
}
diff --git a/src/core/plugins/fixembed/index.ts b/src/core/plugins/fixembed/index.ts
new file mode 100644
index 0000000..eb5b50a
--- /dev/null
+++ b/src/core/plugins/fixembed/index.ts
@@ -0,0 +1,194 @@
+import { defineCorePlugin } from "..";
+import { findByProps } from "@metro";
+import { after } from "@lib/api/patcher";
+import { logger } from "@lib/utils/logger";
+import { settings } from "@lib/api/settings";
+import { React } from "@metro/common";
+
+const { FormSection, FormRow, FormSwitch, FormText } = findByProps(
+ "FormSection",
+ "FormRow",
+ "FormSwitch",
+ "FormText",
+);
+const { ScrollView } = require("react-native");
+
+type FixEmbedSettings = {
+ enabled: boolean;
+ twitter: boolean;
+ instagram: boolean;
+ tiktok: boolean;
+ reddit: boolean;
+};
+
+declare module "@lib/api/settings" {
+ interface Settings {
+ fixembed?: FixEmbedSettings;
+ }
+}
+
+const MessageActions = findByProps("sendMessage");
+let unpatch: (() => void) | null = null;
+
+function transformLinks(content: string, config: FixEmbedSettings): string {
+ let result = content;
+ // Twitter/X: Replace domain, keep path
+ if (config.twitter) {
+ result = result.replace(
+ /https?:\/\/(?:www\.)?(twitter\.com|x\.com)(\/[^\s]+)/gi,
+ "https://fxtwitter.com$2",
+ );
+ }
+ // Instagram: Replace domain, keep path
+ if (config.instagram) {
+ result = result.replace(
+ /https?:\/\/(?:www\.)?instagram\.com(\/[^\s]+)/gi,
+ "https://ddinstagram.com$1",
+ );
+ }
+ // TikTok: Replace domain, keep path
+ if (config.tiktok) {
+ result = result.replace(
+ /https?:\/\/(?:www\.)?tiktok\.com(\/[^\s]+)/gi,
+ "https://tnktok.com$1",
+ );
+ // TikTok short links (vm.tiktok.com)
+ result = result.replace(
+ /https?:\/\/vm\.tiktok\.com\/([A-Za-z0-9]+)\/?/gi,
+ "https://vm.tnktok.com/$1",
+ );
+ }
+ // Reddit: Replace domain, keep path
+ if (config.reddit) {
+ result = result.replace(
+ /https?:\/\/(?:www\.)?reddit\.com(\/[^\s]+)/gi,
+ "https://rxddit.com$1",
+ );
+ }
+ return result;
+}
+
+export default defineCorePlugin({
+ manifest: {
+ id: "bunny.fixembed",
+ version: "1.0.0",
+ type: "plugin",
+ spec: 3,
+ main: "",
+ display: {
+ name: "FixEmbed",
+ description:
+ "Improves social media embeds by using privacy-friendly alternative frontends.",
+ authors: [{ name: "ShiggyCord Team" }],
+ },
+ },
+
+ SettingsComponent() {
+ const { useState, useEffect } = React;
+ const [config, setConfig] = useState({
+ enabled: settings.fixembed?.enabled ?? true,
+ twitter: settings.fixembed?.twitter ?? true,
+ instagram: settings.fixembed?.instagram ?? true,
+ tiktok: settings.fixembed?.tiktok ?? true,
+ reddit: settings.fixembed?.reddit ?? true,
+ });
+
+ useEffect(() => {
+ settings.fixembed = config;
+ }, [config]);
+
+ const updateConfig = (key: keyof FixEmbedSettings, value: boolean) => {
+ setConfig((prev) => ({ ...prev, [key]: value }));
+ };
+
+ return React.createElement(ScrollView, { style: { flex: 1 } }, [
+ React.createElement(
+ FormSection,
+ { title: "General Settings" },
+ React.createElement(FormRow, {
+ label: "Enable FixEmbed",
+ subLabel: "Master switch for all link conversions",
+ trailing: React.createElement(FormSwitch, {
+ value: config.enabled,
+ onValueChange: (v: boolean) => updateConfig("enabled", v),
+ }),
+ }),
+ ),
+ React.createElement(FormSection, { title: "Platforms" }, [
+ React.createElement(FormRow, {
+ label: "Twitter/X",
+ trailing: React.createElement(FormSwitch, {
+ value: config.twitter,
+ disabled: !config.enabled,
+ onValueChange: (v: boolean) => updateConfig("twitter", v),
+ }),
+ }),
+ React.createElement(FormRow, {
+ label: "Instagram",
+ trailing: React.createElement(FormSwitch, {
+ value: config.instagram,
+ disabled: !config.enabled,
+ onValueChange: (v: boolean) => updateConfig("instagram", v),
+ }),
+ }),
+ React.createElement(FormRow, {
+ label: "TikTok",
+ trailing: React.createElement(FormSwitch, {
+ value: config.tiktok,
+ disabled: !config.enabled,
+ onValueChange: (v: boolean) => updateConfig("tiktok", v),
+ }),
+ }),
+ React.createElement(FormRow, {
+ label: "Reddit",
+ trailing: React.createElement(FormSwitch, {
+ value: config.reddit,
+ disabled: !config.enabled,
+ onValueChange: (v: boolean) => updateConfig("reddit", v),
+ }),
+ }),
+ ]),
+ React.createElement(
+ FormSection,
+ { title: "About" },
+ React.createElement(
+ FormText,
+ { style: { padding: 12 } },
+ "This plugin automatically converts social media links to privacy-friendly alternative frontends for better embeds.",
+ ),
+ ),
+ ]);
+ },
+
+ start() {
+ logger.log("FixEmbed: Starting plugin");
+ settings.fixembed = settings.fixembed || {
+ enabled: true,
+ twitter: true,
+ instagram: true,
+ tiktok: true,
+ reddit: true,
+ };
+
+ if (!unpatch) {
+ unpatch = after("sendMessage", MessageActions, (args) => {
+ const config = settings.fixembed!;
+ if (!config.enabled) return;
+ if (args[1]?.content) {
+ args[1].content = transformLinks(args[1].content, config);
+ args[1].nonce = args[1].nonce || Math.random().toString(36).slice(2);
+ }
+ });
+ }
+ logger.log("FixEmbed: Patched outgoing messages");
+ },
+
+ stop() {
+ logger.log("FixEmbed: Stopping plugin");
+ if (unpatch) {
+ unpatch();
+ unpatch = null;
+ }
+ logger.log("FixEmbed: Unpatched outgoing messages");
+ },
+});
diff --git a/src/core/plugins/index.ts b/src/core/plugins/index.ts
index 7b423f7..6657185 100644
--- a/src/core/plugins/index.ts
+++ b/src/core/plugins/index.ts
@@ -1,22 +1,26 @@
import { PluginInstanceInternal } from "@lib/addons/plugins/types";
interface CorePlugin {
- default: PluginInstanceInternal;
- preenabled: boolean;
+ default: PluginInstanceInternal;
+ preenabled: boolean;
}
// Called from @lib/plugins
export const getCorePlugins = (): Record => ({
- "bunny.quickinstall": require("./quickinstall"),
- "bunny.badges": require("./badges"),
- "vendetta.notrack": require("./notrack")
+ "bunny.quickinstall": { ...require("./quickinstall"), preenabled: true },
+ "bunny.badges": require("./badges"),
+ "bunny.notrack": { ...require("./notrack"), preenabled: true },
+ "bunny.messagefix": { ...require("./messagefix") },
+ "bunny.fixembed": { ...require("./fixembed"), preenabled: true },
});
/**
* @internal
*/
-export function defineCorePlugin(instance: PluginInstanceInternal): PluginInstanceInternal {
- // @ts-expect-error
- instance[Symbol.for("bunny.core.plugin")] = true;
- return instance;
+export function defineCorePlugin(
+ instance: PluginInstanceInternal,
+): PluginInstanceInternal {
+ // @ts-expect-error
+ instance[Symbol.for("bunny.core.plugin")] = true;
+ return instance;
}
diff --git a/src/core/plugins/messagefix/index.ts b/src/core/plugins/messagefix/index.ts
new file mode 100644
index 0000000..146bf2b
--- /dev/null
+++ b/src/core/plugins/messagefix/index.ts
@@ -0,0 +1,56 @@
+import { defineCorePlugin } from "..";
+import { findByProps } from "@metro";
+import { logger } from "@lib/utils/logger";
+
+// Find the MessageActions module
+const MessageActions = findByProps("sendMessage");
+let originalSendMessage: Function;
+
+export default defineCorePlugin({
+ manifest: {
+ id: "bunny.messagefix",
+ version: "1.0.0",
+ type: "plugin",
+ spec: 3,
+ main: "",
+ display: {
+ name: "MessageFix",
+ description: "Ensures messages include the required nonce parameter",
+ authors: [{ name: "ShiggyCord Team" }],
+ },
+ },
+
+ start() {
+ // Save original function
+ originalSendMessage = MessageActions.sendMessage;
+
+ // Replace with our implementation
+ MessageActions.sendMessage = function (
+ channelId,
+ message,
+ replyRef,
+ options,
+ ) {
+ // Ensure options exists and has a nonce
+ options = options || {};
+ options.nonce = options.nonce || Date.now().toString();
+
+ // Call original with fixed parameters
+ return originalSendMessage.call(
+ this,
+ channelId,
+ message,
+ replyRef,
+ options,
+ );
+ };
+
+ logger.log("MessageFix: Enabled - adding nonce to all messages");
+ },
+
+ stop() {
+ // Restore original function
+ if (originalSendMessage) MessageActions.sendMessage = originalSendMessage;
+ logger.log("MessageFix: Disabled");
+ },
+});
diff --git a/src/core/plugins/notrack/index.ts b/src/core/plugins/notrack/index.ts
index 58865de..8459303 100644
--- a/src/core/plugins/notrack/index.ts
+++ b/src/core/plugins/notrack/index.ts
@@ -1,36 +1,230 @@
-import network from "./lib/network";
-import console from "./lib/console";
-import miscellanous from "./lib/miscellanous";
-import sentry from "./lib/sentry";
import { defineCorePlugin } from "..";
+import { findByProps } from "@metro";
+import { instead } from "@lib/api/patcher";
import { logger } from "@lib/utils/logger";
-const patches = [
- network(),
- console(),
- miscellanous(),
- // Sentry causes issues
- // sentry(),
-];
+// Type for functions that need to be cleaned up when plugin is disabled
+type PatchCleanupFn = () => void;
+
+// Store all active patches for cleanup
+let patches: PatchCleanupFn[] = [];
+
+// AnalyticsUtils and other tracking utilities
+const AnalyticsUtils = findByProps("AnalyticsActionHandlers");
+const SuperPropUtils = findByProps("encodeProperties", "track");
+const VoiceStateUtils = findByProps("getVoiceStateMetadata");
+const CrashReportUtils = findByProps("submitLiveCrashReport");
+const MetricsUtils = findByProps("_metrics");
+
+// Sentry interface definition to avoid TypeScript errors
+interface SentryClient {
+ getOptions: () => { enabled: boolean };
+ close: () => void;
+ open: () => void;
+}
+
+interface SentryScope {
+ clear: () => void;
+}
+
+interface SentryHub {
+ getClient: () => SentryClient | undefined;
+ getStackTop: () => { scope: SentryScope };
+ getScope: () => SentryScope;
+ addBreadcrumb?: (breadcrumb: unknown) => void;
+}
+
+interface GlobalSentry {
+ hub?: SentryHub;
+}
+
+// Get Sentry objects with proper type checking
+const sentryGlobal = (globalThis as unknown as { __SENTRY__?: GlobalSentry })
+ .__SENTRY__;
+const sentryHub = sentryGlobal?.hub;
+const sentryClient = sentryHub?.getClient();
+
+// Sentry related objects with type safety
+const Sentry = {
+ initializer: findByProps("initSentry"),
+ main: sentryHub,
+ client: sentryClient,
+};
+
+// Utility function to create no-operation patches
+const noop = (prop: string, parent: Record): PatchCleanupFn => {
+ try {
+ return instead(prop, parent, () => undefined);
+ } catch (e) {
+ return () => false;
+ }
+};
+
+// Network tracking prevention
+function patchNetwork(): PatchCleanupFn {
+ // Analytics and tracking endpoints to block
+ const analyticsTest =
+ /client-analytics\.braintreegateway\.com|discord\.com\/api\/v9\/(science|track)|app\.adjust\..*|.*\.ingest\.sentry\.io/;
+
+ try {
+ const unpatch = instead(
+ "send",
+ XMLHttpRequest.prototype,
+ function (
+ this: XMLHttpRequest & { __sentry_xhr__?: { url: string } },
+ args: unknown[],
+ orig: Function,
+ ) {
+ if (
+ this.__sentry_xhr__?.url &&
+ analyticsTest.test(this.__sentry_xhr__.url)
+ ) {
+ return undefined;
+ }
+ return orig.apply(this, args);
+ },
+ );
+ return unpatch;
+ } catch (e) {
+ return () => false;
+ }
+}
+
+// Console patching to remove Sentry wrappers
+function patchConsole(): PatchCleanupFn {
+ type ConsoleFunction = Function & { __sentry_original__?: Function };
+ type ConsoleFunctions = Record;
+
+ const sentrified: ConsoleFunctions = {};
+
+ try {
+ // We need to use Object.keys because TypeScript doesn't allow indexing Console with strings
+ (Object.keys(console) as Array).forEach((key) => {
+ const consoleFunc = (console as unknown as ConsoleFunctions)[key];
+ if (consoleFunc) {
+ sentrified[key] = consoleFunc;
+ const originalFunc = consoleFunc.__sentry_original__;
+ (console as unknown as ConsoleFunctions)[key] =
+ originalFunc ?? consoleFunc;
+ }
+ });
+ } catch (e) {
+ logger.log("Failed to de-sentrify console functions!", e);
+ }
+
+ return () => {
+ Object.keys(sentrified).forEach((key) => {
+ if (sentrified[key]) {
+ (console as unknown as ConsoleFunctions)[key] = sentrified[key];
+ }
+ });
+ };
+}
+
+// Miscellaneous tracking functions
+function patchMiscellaneous(): PatchCleanupFn {
+ const miscPatches = [
+ // Global analytics utilities
+ AnalyticsUtils?.AnalyticsActionHandlers &&
+ noop("handleTrack", AnalyticsUtils.AnalyticsActionHandlers),
+ AnalyticsUtils?.AnalyticsActionHandlers &&
+ noop("handleFingerprint", AnalyticsUtils.AnalyticsActionHandlers),
+
+ // Super properties tracking
+ SuperPropUtils && noop("track", SuperPropUtils),
+
+ // Voice state metadata tracking
+ VoiceStateUtils && noop("trackWithMetadata", VoiceStateUtils),
+
+ // Crash reporter
+ CrashReportUtils && noop("submitLiveCrashReport", CrashReportUtils),
+
+ // Metrics
+ MetricsUtils?._metrics && noop("push", MetricsUtils._metrics),
+ ].filter(Boolean) as PatchCleanupFn[];
+
+ return () => miscPatches.forEach((p) => p());
+}
+
+// Sentry error reporting prevention
+function patchSentry(): PatchCleanupFn {
+ const sentryPatches: PatchCleanupFn[] = [];
+
+ // Add patches if Sentry components exist
+ if (Sentry.initializer) {
+ sentryPatches.push(noop("initSentry", Sentry.initializer));
+ }
+
+ if (Sentry.main && Sentry.main.addBreadcrumb) {
+ sentryPatches.push(noop("addBreadcrumb", Sentry.main));
+ }
+
+ // Disable Sentry client
+ if (Sentry.client) {
+ try {
+ Sentry.client.getOptions().enabled = false;
+ Sentry.client.close();
+
+ if (Sentry.main) {
+ if (Sentry.main.getStackTop) {
+ Sentry.main.getStackTop().scope.clear();
+ }
+
+ if (Sentry.main.getScope) {
+ Sentry.main.getScope().clear();
+ }
+ }
+ } catch (e) {
+ // Silent fail
+ }
+ }
+
+ return () => {
+ try {
+ sentryPatches.forEach((p) => p());
+
+ // Re-enable Sentry client on cleanup
+ if (Sentry.client) {
+ Sentry.client.getOptions().enabled = true;
+ Sentry.client.open();
+ }
+ } catch (e) {
+ // Silent fail
+ }
+ };
+}
export default defineCorePlugin({
- manifest: {
- id: "vendetta.notrack",
- version: "1.0.0",
- type: "plugin",
- spec: 3,
- main: "",
- display: {
- name: "NoTrack",
- description: "Disables Discord's telemetry",
- authors: [{ name: "maisymoe" }]
- }
+ manifest: {
+ id: "bunny.notrack",
+ version: "1.0.0",
+ type: "plugin",
+ spec: 3,
+ main: "",
+ display: {
+ name: "NoTrack",
+ description: "Disables Discord's telemetry",
+ authors: [{ name: "maisymoe" }],
},
- start() {
- network(),
- console(),
- miscellanous(),
- sentry()
- }
-});
+ },
+ start() {
+ // Apply all patches
+ patches = [
+ patchNetwork(),
+ patchConsole(),
+ patchMiscellaneous(),
+ patchSentry(),
+ ].filter(Boolean);
+
+ logger.log("NoTrack: Enabled - all telemetry tracking disabled");
+ },
+
+ stop() {
+ // Call all cleanup functions
+ patches.forEach((p) => p?.());
+ patches = [];
+
+ logger.log("NoTrack: Disabled - telemetry tracking restored");
+ },
+});
diff --git a/src/core/plugins/notrack/lib/console.ts b/src/core/plugins/notrack/lib/console.ts
deleted file mode 100644
index 7a69471..0000000
--- a/src/core/plugins/notrack/lib/console.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { logger } from "@lib/utils/logger";
-
-export default () => {
- const sentrified = {};
-
- try {
- Object.keys(console).forEach((x) => {
- sentrified[x] = console[x];
- console[x] = console[x].__sentry_original__ ?? console[x];
- });
- } catch(e) {
- logger.log("Failed to de-sentrify console functions!", e);
- };
-
- return () => Object.keys(console).forEach((x) => console[x] = sentrified[x]);
-}
diff --git a/src/core/plugins/notrack/lib/miscellanous.ts b/src/core/plugins/notrack/lib/miscellanous.ts
deleted file mode 100644
index b6a6093..0000000
--- a/src/core/plugins/notrack/lib/miscellanous.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { findByProps } from "@metro";
-import { noop } from "./utils";
-
-// Stuff in here is non-specific tracking scattered around the app
-
-const AnalyticsUtils = findByProps("AnalyticsActionHandlers");
-const SuperPropUtils = findByProps("encodeProperties", "track");
-const VoiceStateUtils = findByProps("getVoiceStateMetadata");
-const CrashReportUtils = findByProps("submitLiveCrashReport");
-const MetricsUtils = findByProps("_metrics");
-
-export default () => {
- const patches = [
- // Global analytics utilities
- ...(["handleTrack", "handleFingerprint"].map((i) => noop(i, AnalyticsUtils.AnalyticsActionHandlers))),
- // TODO: This is a getter - look into patching it
- // noop("analyticsTrackingStoreMaker", AnalyticsUtils),
-
- // Super properties tracking
- noop("track", SuperPropUtils),
-
- // Voice state metadata tracking
- noop("trackWithMetadata", VoiceStateUtils),
-
- // Crash reporter
- noop("submitLiveCrashReport", CrashReportUtils),
-
- // Whatever "metrics" implies
- noop("push", MetricsUtils._metrics),
- ];
-
- return () => patches.forEach((p) => p());
-}
diff --git a/src/core/plugins/notrack/lib/network.ts b/src/core/plugins/notrack/lib/network.ts
deleted file mode 100644
index 68f762e..0000000
--- a/src/core/plugins/notrack/lib/network.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { instead } from "@lib/api/patcher";
-
-export default () => {
- // https://github.com/yellowsink/shelter-plugins/blob/3a62d1557b704f4617f72882edba826218eb8c6e/plugins/antitrack/index.js#L10-L25
- const analyticsTest = /client-analytics\.braintreegateway\.com|discord\.com\/api\/v9\/(science|track)|app\.adjust\..*|.*\.ingest\.sentry\.io/;
-
- return instead(
- "send",
- XMLHttpRequest.prototype,
- function (args, orig) { if (!analyticsTest.test(this.__sentry_xhr__?.url)) return orig.apply(this, args) },
- );
-}
diff --git a/src/core/plugins/notrack/lib/sentry.ts b/src/core/plugins/notrack/lib/sentry.ts
deleted file mode 100644
index 5b01b7e..0000000
--- a/src/core/plugins/notrack/lib/sentry.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { findByProps } from "@metro";
-import { noop } from "./utils";
-
-const Sentry = {
- initializer: findByProps("initSentry"),
- main: globalThis.__SENTRY__?.hub,
- client: globalThis.__SENTRY__?.hub?.getClient(),
-};
-
-export default () => {
- const patches = [
- noop("initSentry", Sentry.initializer),
- noop("addBreadcrumb", Sentry.main),
- ];
-
- Sentry.client.getOptions().enabled = false;
- Sentry.client.close();
- Sentry.main.getStackTop().scope.clear();
- Sentry.main.getScope().clear();
-
- return () => {
- patches.forEach((p) => p());
- Sentry.client.getOptions().enabled = true;
- Sentry.client.open();
- };
-}
diff --git a/src/core/plugins/notrack/lib/utils.ts b/src/core/plugins/notrack/lib/utils.ts
deleted file mode 100644
index 55f871f..0000000
--- a/src/core/plugins/notrack/lib/utils.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { instead } from "@lib/api/patcher";
-
-export const noop = (prop: string, parent: any) => instead(prop, parent, () => {});
diff --git a/src/core/plugins/quickinstall/forumPost.tsx b/src/core/plugins/quickinstall/forumPost.tsx
index 25665ad..ad6fb7e 100644
--- a/src/core/plugins/quickinstall/forumPost.tsx
+++ b/src/core/plugins/quickinstall/forumPost.tsx
@@ -5,7 +5,13 @@ import { installTheme, removeTheme, themes } from "@lib/addons/themes";
import { findAssetId } from "@lib/api/assets";
import { isThemeSupported } from "@lib/api/native/loader";
import { after } from "@lib/api/patcher";
-import { HTTP_REGEX_MULTI, VD_DISCORD_SERVER_ID, VD_PLUGINS_CHANNEL_ID, VD_PROXY_PREFIX, VD_THEMES_CHANNEL_ID } from "@lib/utils/constants";
+import {
+ HTTP_REGEX_MULTI,
+ VD_DISCORD_SERVER_ID,
+ VD_PLUGINS_CHANNEL_ID,
+ VD_PROXY_PREFIX,
+ VD_THEMES_CHANNEL_ID,
+} from "@lib/utils/constants";
import { lazyDestructure } from "@lib/utils/lazy";
import { Button } from "@metro/common/components";
import { findByProps, findByPropsLazy } from "@metro/wrappers";
@@ -16,74 +22,91 @@ type PostType = "Plugin" | "Theme";
// const ForumPostLongPressActionSheet = findByNameProxy("ForumPostLongPressActionSheet", false);
// const { ActionSheetRow } = findByPropsProxy("ActionSheetRow");
-const { useFirstForumPostMessage } = lazyDestructure(() => findByProps("useFirstForumPostMessage"));
+const { useFirstForumPostMessage } = lazyDestructure(() =>
+ findByProps("useFirstForumPostMessage"),
+);
const forumReactions = findByPropsLazy("MostCommonForumPostReaction");
const postMap = {
- Plugin: {
- storage: VdPluginManager.plugins,
- urlsFilter: (url: string) => url.startsWith(VD_PROXY_PREFIX),
- installOrRemove: (url: string) => {
- const isInstalled = postMap.Plugin.storage[url];
- return isInstalled ? VdPluginManager.removePlugin(url) : VdPluginManager.installPlugin(url);
- }
+ Plugin: {
+ storage: VdPluginManager.plugins,
+ urlsFilter: (url: string) => url.startsWith(VD_PROXY_PREFIX),
+ installOrRemove: (url: string) => {
+ const isInstalled = postMap.Plugin.storage[url];
+ return isInstalled
+ ? VdPluginManager.removePlugin(url)
+ : VdPluginManager.installPlugin(url);
},
- Theme: {
- storage: themes,
- urlsFilter: (url: string) => url.endsWith(".json"),
- installOrRemove: (url: string) => {
- const isInstalled = postMap.Theme.storage[url];
- return isInstalled ? removeTheme(url) : installTheme(url);
- },
- }
+ },
+ Theme: {
+ storage: themes,
+ urlsFilter: (url: string) => url.endsWith(".json"),
+ installOrRemove: (url: string) => {
+ const isInstalled = postMap.Theme.storage[url];
+ return isInstalled ? removeTheme(url) : installTheme(url);
+ },
+ },
};
-function useExtractThreadContent(thread: any, _firstMessage = null, actionSheet = false): ([PostType, string]) | void {
- if (thread.guild_id !== VD_DISCORD_SERVER_ID) return;
+function useExtractThreadContent(
+ thread: any,
+ _firstMessage = null,
+ actionSheet = false,
+): [PostType, string] | void {
+ if (thread.guild_id !== VD_DISCORD_SERVER_ID) return;
- // Determine what type of addon this is.
- let postType: PostType;
- if (thread.parent_id === VD_PLUGINS_CHANNEL_ID) {
- postType = "Plugin";
- } else if (thread.parent_id === VD_THEMES_CHANNEL_ID && isThemeSupported()) {
- postType = "Theme";
- } else return;
+ // Determine what type of addon this is.
+ let postType: PostType;
+ if (thread.parent_id === VD_PLUGINS_CHANNEL_ID) {
+ postType = "Plugin";
+ } else if (thread.parent_id === VD_THEMES_CHANNEL_ID && isThemeSupported()) {
+ postType = "Theme";
+ } else return;
- const { firstMessage } = actionSheet ? useFirstForumPostMessage(thread) : { firstMessage: _firstMessage };
+ const { firstMessage } = actionSheet
+ ? useFirstForumPostMessage(thread)
+ : { firstMessage: _firstMessage };
- const urls = firstMessage?.content?.match(HTTP_REGEX_MULTI)?.filter(postMap[postType].urlsFilter);
- if (!urls || !urls[0]) return;
+ const urls = firstMessage?.content
+ ?.match(HTTP_REGEX_MULTI)
+ ?.filter(postMap[postType].urlsFilter);
+ if (!urls || !urls[0]) return;
- // Sync with lib/managers/plugins
- if (postType === "Plugin" && !urls[0].endsWith("/")) urls[0] += "/";
+ // Sync with lib/managers/plugins
+ if (postType === "Plugin" && !urls[0].endsWith("/")) urls[0] += "/";
- return [postType, urls[0]];
+ return [postType, urls[0]];
}
-function useInstaller(thread: any, firstMessage = null, actionSheet = false): [true] | [false, PostType, boolean, boolean, () => Promise] {
- const [postType, url] = useExtractThreadContent(thread, firstMessage, actionSheet) ?? [];
+function useInstaller(
+ thread: any,
+ firstMessage = null,
+ actionSheet = false,
+): [true] | [false, PostType, boolean, boolean, () => Promise] {
+ const [postType, url] =
+ useExtractThreadContent(thread, firstMessage, actionSheet) ?? [];
- useProxy(VdPluginManager.plugins);
- useProxy(themes);
+ useProxy(VdPluginManager.plugins);
+ useProxy(themes);
- const [isInstalling, setIsInstalling] = React.useState(false);
+ const [isInstalling, setIsInstalling] = React.useState(false);
- if (!postType || !url) return [true];
+ if (!postType || !url) return [true];
- const isInstalled = Boolean(postMap[postType].storage[url]);
+ const isInstalled = Boolean(postMap[postType].storage[url]);
- const installOrRemove = async () => {
- setIsInstalling(true);
- try {
- await postMap[postType].installOrRemove(url);
- } catch (e: any) {
- showToast(e.message, findAssetId("Small"));
- } finally {
- setIsInstalling(false);
- }
- };
+ const installOrRemove = async () => {
+ setIsInstalling(true);
+ try {
+ await postMap[postType].installOrRemove(url);
+ } catch (e: any) {
+ showToast(e.message, findAssetId("Small"));
+ } finally {
+ setIsInstalling(false);
+ }
+ };
- return [false, postType, isInstalled, isInstalling, installOrRemove];
+ return [false, postType, isInstalled, isInstalling, installOrRemove];
}
// // apparently broken???
@@ -104,33 +127,43 @@ function useInstaller(thread: any, firstMessage = null, actionSheet = false): [t
// );
// });
-const installButtonPatch = () => after("MostCommonForumPostReaction", forumReactions, ([{ thread, firstMessage }], res) => {
- const [shouldReturn, _, installed, loading, installOrRemove] = useInstaller(thread, firstMessage, true);
- if (shouldReturn) return;
+const installButtonPatch = () =>
+ after(
+ "MostCommonForumPostReaction",
+ forumReactions,
+ ([{ thread, firstMessage }], res) => {
+ const [shouldReturn, _, installed, loading, installOrRemove] =
+ useInstaller(thread, firstMessage, true);
+ if (shouldReturn) return;
- return <>
- {res}
-
+ return (
+ <>
+ {res}
+
-
- >;
-});
+
+ >
+ );
+ },
+ );
export default () => {
- const patches = [
- // actionSheetPatch(),
- installButtonPatch()
- ];
+ const patches = [
+ // actionSheetPatch(),
+ installButtonPatch(),
+ ];
- return () => patches.map(p => p());
+ return () => patches.map((p) => p());
};
diff --git a/src/core/plugins/quickinstall/index.ts b/src/core/plugins/quickinstall/index.ts
index d8f25da..f892622 100644
--- a/src/core/plugins/quickinstall/index.ts
+++ b/src/core/plugins/quickinstall/index.ts
@@ -5,22 +5,22 @@ import patchUrl from "./url";
let patches = [] as (() => unknown)[];
export default defineCorePlugin({
- manifest: {
- id: "bunny.quickinstall",
- version: "1.0.0",
- type: "plugin",
- spec: 3,
- main: "",
- display: {
- name: "QuickInstall",
- description: "Quickly install Vendetta plugins and themes",
- authors: [{ name: "Vendetta Team" }]
- }
+ manifest: {
+ id: "bunny.quickinstall",
+ version: "1.0.0",
+ type: "plugin",
+ spec: 3,
+ main: "",
+ display: {
+ name: "QuickInstall",
+ description: "Quickly install Vendetta plugins and themes",
+ authors: [{ name: "Vendetta Team" }],
},
- start() {
- patches = [patchForumPost(), patchUrl()];
- },
- stop() {
- patches.forEach(p => p());
- }
+ },
+ start() {
+ patches = [patchForumPost(), patchUrl()];
+ },
+ stop() {
+ patches.forEach((p) => p());
+ },
});
diff --git a/src/core/plugins/quickinstall/url.tsx b/src/core/plugins/quickinstall/url.tsx
index 5e46ee1..24a398e 100644
--- a/src/core/plugins/quickinstall/url.tsx
+++ b/src/core/plugins/quickinstall/url.tsx
@@ -14,71 +14,79 @@ import { findByProps, findByPropsLazy } from "@metro/wrappers";
import { showToast } from "@ui/toasts";
import { Linking } from "react-native";
-const showSimpleActionSheet = findExports(byMutableProp("showSimpleActionSheet"));
+const showSimpleActionSheet = findExports(
+ byMutableProp("showSimpleActionSheet"),
+);
const handleClick = findByPropsLazy("handleClick");
const { getChannelId } = lazyDestructure(() => channels);
const { getChannel } = lazyDestructure(() => findByProps("getChannel"));
function typeFromUrl(url: string) {
- if (url.startsWith(VD_PROXY_PREFIX)) {
- return "plugin";
- } else if (url.endsWith(".json") && isThemeSupported()) {
- return "theme";
- }
+ if (url.startsWith(VD_PROXY_PREFIX)) {
+ return "plugin";
+ } else if (url.endsWith(".json") && isThemeSupported()) {
+ return "theme";
+ }
}
function installWithToast(type: "plugin" | "theme", url: string) {
- (type === "plugin" ? VdPluginManager.installPlugin.bind(VdPluginManager) : installTheme)(url)
- .then(() => {
- showToast(Strings.SUCCESSFULLY_INSTALLED, findAssetId("Check"));
- })
- .catch((e: Error) => {
- showToast(e.message, findAssetId("Small"));
- });
+ (type === "plugin"
+ ? VdPluginManager.installPlugin.bind(VdPluginManager)
+ : installTheme)(url)
+ .then(() => {
+ showToast(Strings.SUCCESSFULLY_INSTALLED, findAssetId("Check"));
+ })
+ .catch((e: Error) => {
+ showToast(e.message, findAssetId("Small"));
+ });
}
export default () => {
- const patches = new Array();
+ const patches = new Array();
- patches.push(
- after("showSimpleActionSheet", showSimpleActionSheet, args => {
- if (args[0].key !== "LongPressUrl") return;
- const {
- header: { title: url },
- options,
- } = args[0];
+ patches.push(
+ after("showSimpleActionSheet", showSimpleActionSheet, (args) => {
+ if (args[0].key !== "LongPressUrl") return;
+ const {
+ header: { title: url },
+ options,
+ } = args[0];
- const urlType = typeFromUrl(url);
- if (!urlType) return;
+ const urlType = typeFromUrl(url);
+ if (!urlType) return;
- options.push({
- label: Strings.INSTALL_ADDON,
- onPress: () => installWithToast(urlType, url),
- });
- })
- );
+ options.push({
+ label: Strings.INSTALL_ADDON,
+ onPress: () => installWithToast(urlType, url),
+ });
+ }),
+ );
- patches.push(
- instead("handleClick", handleClick, async function (this: any, args, orig) {
- const { href: url } = args[0];
+ patches.push(
+ instead("handleClick", handleClick, async function (this: any, args, orig) {
+ const { href: url } = args[0];
- const urlType = typeFromUrl(url);
- if (!urlType) return orig.apply(this, args);
+ const urlType = typeFromUrl(url);
+ if (!urlType) return orig.apply(this, args);
- // Make clicking on theme links only work in #themes, should there be a theme proxy in the future, this can be removed.
- if (urlType === "theme" && getChannel(getChannelId())?.parent_id !== VD_THEMES_CHANNEL_ID) return orig.apply(this, args);
+ // Make clicking on theme links only work in #themes, should there be a theme proxy in the future, this can be removed.
+ if (
+ urlType === "theme" &&
+ getChannel(getChannelId())?.parent_id !== VD_THEMES_CHANNEL_ID
+ )
+ return orig.apply(this, args);
- showConfirmationAlert({
- title: Strings.HOLD_UP,
- content: formatString("CONFIRMATION_LINK_IS_A_TYPE", { urlType }),
- onConfirm: () => installWithToast(urlType, url),
- confirmText: Strings.INSTALL,
- cancelText: Strings.CANCEL,
- secondaryConfirmText: Strings.OPEN_IN_BROWSER,
- onConfirmSecondary: () => Linking.openURL(url),
- });
- })
- );
+ showConfirmationAlert({
+ title: Strings.HOLD_UP,
+ content: formatString("CONFIRMATION_LINK_IS_A_TYPE", { urlType }),
+ onConfirm: () => installWithToast(urlType, url),
+ confirmText: Strings.INSTALL,
+ cancelText: Strings.CANCEL,
+ secondaryConfirmText: Strings.OPEN_IN_BROWSER,
+ onConfirmSecondary: () => Linking.openURL(url),
+ });
+ }),
+ );
- return () => patches.forEach(p => p());
+ return () => patches.forEach((p) => p());
};
diff --git a/src/core/ui/components/AddonCard.tsx b/src/core/ui/components/AddonCard.tsx
index a0486d4..a58445d 100644
--- a/src/core/ui/components/AddonCard.tsx
+++ b/src/core/ui/components/AddonCard.tsx
@@ -1,154 +1,176 @@
import { findAssetId } from "@lib/api/assets";
import { lazyDestructure } from "@lib/utils/lazy";
-import { Card, FormRadio, FormSwitch, IconButton, LegacyFormRow, Stack, Text } from "@metro/common/components";
+import {
+ Card,
+ FormRadio,
+ FormSwitch,
+ IconButton,
+ LegacyFormRow,
+ Stack,
+ Text,
+} from "@metro/common/components";
import { findByProps } from "@metro/wrappers";
import { semanticColors } from "@ui/color";
import { createStyles, TextStyleSheet } from "@ui/styles";
import { TouchableOpacity, View } from "react-native";
-const { hideActionSheet } = lazyDestructure(() => findByProps("openLazy", "hideActionSheet"));
-const { showSimpleActionSheet } = lazyDestructure(() => findByProps("showSimpleActionSheet"));
+const { hideActionSheet } = lazyDestructure(() =>
+ findByProps("openLazy", "hideActionSheet"),
+);
+const { showSimpleActionSheet } = lazyDestructure(() =>
+ findByProps("showSimpleActionSheet"),
+);
// TODO: These styles work weirdly. Low DPI Has cramped text. Fix?
const useStyles = createStyles({
- card: {
- backgroundColor: semanticColors?.CARD_SECONDARY_BG,
- borderRadius: 12,
- overflow: "hidden"
- },
- header: {
- padding: 0,
- },
- headerLeading: {
- flexDirection: "column",
- justifyContent: "center",
- scale: 1.2
- },
- headerTrailing: {
- display: "flex",
- flexDirection: "row",
- gap: 15,
- alignItems: "center"
- },
- headerLabel: {
- ...TextStyleSheet["heading-md/semibold"],
- color: semanticColors.TEXT_NORMAL,
- },
- headerSubtitle: {
- ...TextStyleSheet["text-md/semibold"],
- color: semanticColors.TEXT_MUTED,
- },
- descriptionLabel: {
- ...TextStyleSheet["text-md/semibold"],
- color: semanticColors.TEXT_NORMAL,
- },
- actions: {
- flexDirection: "row-reverse",
- alignItems: "center",
- gap: 5
- },
- iconStyle: {
- tintColor: semanticColors.LOGO_PRIMARY,
- opacity: 0.2,
- height: 64,
- width: 64,
- left: void 0,
- right: "30%",
- top: "-10%"
- }
+ card: {
+ backgroundColor: semanticColors?.CARD_SECONDARY_BG,
+ borderRadius: 12,
+ overflow: "hidden",
+ },
+ header: {
+ padding: 0,
+ },
+ headerLeading: {
+ flexDirection: "column",
+ justifyContent: "center",
+ scale: 1.2,
+ },
+ headerTrailing: {
+ display: "flex",
+ flexDirection: "row",
+ gap: 15,
+ alignItems: "center",
+ },
+ headerLabel: {
+ ...TextStyleSheet["heading-md/semibold"],
+ color: semanticColors.TEXT_NORMAL,
+ },
+ headerSubtitle: {
+ ...TextStyleSheet["text-md/semibold"],
+ color: semanticColors.TEXT_MUTED,
+ },
+ descriptionLabel: {
+ ...TextStyleSheet["text-md/semibold"],
+ color: semanticColors.TEXT_NORMAL,
+ },
+ actions: {
+ flexDirection: "row-reverse",
+ alignItems: "center",
+ gap: 5,
+ },
+ iconStyle: {
+ tintColor: semanticColors.LOGO_PRIMARY,
+ opacity: 0.2,
+ height: 64,
+ width: 64,
+ left: void 0,
+ right: "30%",
+ top: "-10%",
+ },
});
interface Action {
- icon: string;
- disabled?: boolean;
- onPress: () => void;
+ icon: string;
+ disabled?: boolean;
+ onPress: () => void;
}
interface OverflowAction extends Action {
- label: string;
- isDestructive?: boolean;
+ label: string;
+ isDestructive?: boolean;
}
export interface CardWrapper {
- item: T;
- result: Fuzzysort.KeysResult;
+ item: T;
+ result: Fuzzysort.KeysResult;
}
interface CardProps {
- index?: number;
- headerLabel: string;
- headerSublabel?: string;
- headerIcon?: string;
- toggleType?: "switch" | "radio";
- toggleValue: () => boolean;
- onToggleChange?: (v: boolean) => void;
- descriptionLabel?: string;
- actions?: Action[];
- overflowTitle?: string;
- overflowActions?: OverflowAction[];
+ index?: number;
+ headerLabel: string;
+ headerSublabel?: string;
+ headerIcon?: string;
+ toggleType?: "switch" | "radio";
+ toggleValue: () => boolean;
+ onToggleChange?: (v: boolean) => void;
+ descriptionLabel?: string;
+ actions?: Action[];
+ overflowTitle?: string;
+ overflowActions?: OverflowAction[];
}
export default function AddonCard(props: CardProps) {
- const styles = useStyles();
+ const styles = useStyles();
- return (
-
-
-
-
- {props.headerLabel}
- {props.headerSublabel && (
- {props.headerSublabel}
- )}
-
-
-
- {props.overflowActions &&
- showSimpleActionSheet({
- key: "CardOverflow",
- header: {
- title: props.overflowTitle,
- icon: props.headerIcon && ,
- onClose: () => hideActionSheet(),
- },
- options: props.overflowActions?.map(i => ({
- ...i,
- icon: findAssetId(i.icon)
- })),
- })}
- size="sm"
- variant="secondary"
- icon={findAssetId("CircleInformationIcon-primary")}
- />}
- {props.actions?.map(({ icon, onPress, disabled }) => (
-
- ))}
-
- {props.toggleType && (props.toggleType === "switch" ?
-
- :
- {
- props.onToggleChange?.(!props.toggleValue());
- }}>
-
-
- )}
-
-
- {props.descriptionLabel &&
- {props.descriptionLabel}
- }
-
-
- );
+ return (
+
+
+
+
+ {props.headerLabel}
+ {props.headerSublabel && (
+ {props.headerSublabel}
+ )}
+
+
+
+ {props.overflowActions && (
+
+ showSimpleActionSheet({
+ key: "CardOverflow",
+ header: {
+ title: props.overflowTitle,
+ icon: props.headerIcon && (
+
+ ),
+ },
+ options: props.overflowActions?.map((i) => ({
+ ...i,
+ icon: findAssetId(i.icon),
+ })),
+ })
+ }
+ size="sm"
+ variant="secondary"
+ icon={findAssetId("CircleInformationIcon-primary")}
+ />
+ )}
+ {props.actions?.map(({ icon, onPress, disabled }) => (
+
+ ))}
+
+ {props.toggleType &&
+ (props.toggleType === "switch" ? (
+
+ ) : (
+ {
+ props.onToggleChange?.(!props.toggleValue());
+ }}
+ >
+
+
+ ))}
+
+
+ {props.descriptionLabel && (
+ {props.descriptionLabel}
+ )}
+
+
+ );
}
diff --git a/src/core/ui/components/AddonPage.tsx b/src/core/ui/components/AddonPage.tsx
index 8d624a5..24e2059 100644
--- a/src/core/ui/components/AddonPage.tsx
+++ b/src/core/ui/components/AddonPage.tsx
@@ -7,217 +7,294 @@ import isValidHttpUrl from "@lib/utils/isValidHttpUrl";
import { lazyDestructure, proxyLazy } from "@lib/utils/lazy";
import { findByProps } from "@metro";
import { clipboard, NavigationNative } from "@metro/common";
-import { AlertActionButton, AlertModal, Button, FlashList, FloatingActionButton, HelpMessage, IconButton, Stack, Text, TextInput, useSafeAreaInsets } from "@metro/common/components";
+import {
+ AlertActionButton,
+ AlertModal,
+ Button,
+ FlashList,
+ FloatingActionButton,
+ HelpMessage,
+ IconButton,
+ Stack,
+ Text,
+ TextInput,
+ useSafeAreaInsets,
+} from "@metro/common/components";
import { ErrorBoundary, Search } from "@ui/components";
import { isNotNil } from "es-toolkit";
import fuzzysort from "fuzzysort";
-import { ComponentType, ReactNode, useCallback, useEffect, useMemo } from "react";
+import {
+ ComponentType,
+ ReactNode,
+ useCallback,
+ useEffect,
+ useMemo,
+} from "react";
import { Image, ScrollView, View } from "react-native";
-const { showSimpleActionSheet, hideActionSheet } = lazyDestructure(() => findByProps("showSimpleActionSheet"));
+const { showSimpleActionSheet, hideActionSheet } = lazyDestructure(() =>
+ findByProps("showSimpleActionSheet"),
+);
type SearchKeywords = Array string)>;
interface AddonPageProps {
- title: string;
- items: I[];
- searchKeywords: SearchKeywords;
- sortOptions?: Record number>;
- resolveItem?: (value: I) => T | undefined;
- safeModeHint?: {
- message?: string;
- footer?: ReactNode;
- };
- installAction?: {
- label?: string;
- // Ignored when onPress is defined!
- fetchFn?: (url: string) => Promise;
- onPress?: () => void;
- };
+ title: string;
+ items: I[];
+ searchKeywords: SearchKeywords;
+ sortOptions?: Record number>;
+ resolveItem?: (value: I) => T | undefined;
+ safeModeHint?: {
+ message?: string;
+ footer?: ReactNode;
+ };
+ installAction?: {
+ label?: string;
+ // Ignored when onPress is defined!
+ fetchFn?: (url: string) => Promise;
+ onPress?: () => void;
+ };
- OptionsActionSheetComponent?: ComponentType;
+ OptionsActionSheetComponent?: ComponentType;
- CardComponent: ComponentType>;
- ListHeaderComponent?: ComponentType;
- ListFooterComponent?: ComponentType;
+ CardComponent: ComponentType>;
+ ListHeaderComponent?: ComponentType;
+ ListFooterComponent?: ComponentType;
}
-function InputAlert(props: { label: string, fetchFn: (url: string) => Promise; }) {
- const [value, setValue] = React.useState("");
- const [error, setError] = React.useState("");
- const [isFetching, setIsFetching] = React.useState(false);
+function InputAlert(props: {
+ label: string;
+ fetchFn: (url: string) => Promise;
+}) {
+ const [value, setValue] = React.useState("");
+ const [error, setError] = React.useState("");
+ const [isFetching, setIsFetching] = React.useState(false);
- function onConfirmWrapper() {
- setIsFetching(true);
+ function onConfirmWrapper() {
+ setIsFetching(true);
- props.fetchFn(value)
- .then(() => dismissAlert("AddonInputAlert"))
- .catch((e: unknown) => e instanceof Error ? setError(e.message) : String(e))
- .finally(() => setIsFetching(false));
- }
+ props
+ .fetchFn(value)
+ .then(() => dismissAlert("AddonInputAlert"))
+ .catch((e: unknown) =>
+ e instanceof Error ? setError(e.message) : String(e),
+ )
+ .finally(() => setIsFetching(false));
+ }
- return
- {
- setValue(v);
- if (error) setError("");
- }}
- returnKeyType="done"
- onSubmitEditing={onConfirmWrapper}
- state={error ? "error" : undefined}
- errorMessage={error || undefined}
- />
-
-
-
- }
- actions={
-
- {/* Manual button as we don't want alert to immediately dismiss when we tap on it */}
-
-
-
- }
- />;
-}
-
-export default function AddonPage({ CardComponent, ...props }: AddonPageProps) {
- const [search, setSearch] = React.useState("");
- const [sortFn, setSortFn] = React.useState<((a: T, b: T) => number) | null>(() => null);
- const { bottom: bottomInset } = useSafeAreaInsets();
- const navigation = NavigationNative.useNavigation();
-
- useEffect(() => {
- if (props.OptionsActionSheetComponent) {
- navigation.setOptions({
- headerRight: () => showSheet("AddonMoreSheet", props.OptionsActionSheetComponent!)}
- />
- });
- }
- }, [navigation]);
-
- const results = useMemo(() => {
- let values = props.items;
- if (props.resolveItem) values = values.map(props.resolveItem).filter(isNotNil);
- const items = values.filter(i => isNotNil(i) && typeof i === "object");
- if (!search && sortFn) items.sort(sortFn);
-
- return fuzzysort.go(search, items, { keys: props.searchKeywords, all: true });
- }, [props.items, sortFn, search]);
-
- const onInstallPress = useCallback(() => {
- if (!props.installAction) return () => { };
- const { label, onPress, fetchFn } = props.installAction;
- if (fetchFn) {
- openAlert("AddonInputAlert", );
- } else {
- onPress?.();
- }
- }, []);
-
- if (results.length === 0 && !search) {
- return
-
-
-
- Oops! Nothing to see hereโฆ yet!
-
-
+ return (
+
+ {
+ setValue(v);
+ if (error) setError("");
+ }}
+ returnKeyType="done"
+ onSubmitEditing={onConfirmWrapper}
+ state={error ? "error" : undefined}
+ errorMessage={error || undefined}
+ />
+
;
- }
-
- const headerElement = (
-
- {settings.safeMode?.enabled &&
-
- {props.safeModeHint?.message}
-
- {props.safeModeHint?.footer}
- }
-
- setSearch(v)} />
- {props.sortOptions && showSimpleActionSheet({
- key: "AddonListSortOptions",
- header: {
- title: "Sort Options",
- onClose: () => hideActionSheet("AddonListSortOptions"),
- },
- options: Object.entries(props.sortOptions!).map(([name, fn]) => ({
- label: name,
- onPress: () => setSortFn(() => fn)
- }))
- })}
- />}
-
- {props.ListHeaderComponent && }
-
- );
-
- return (
-
-
-
-
- Hmmm... could not find that!
-
- }
- contentContainerStyle={{ padding: 8, paddingHorizontal: 12, paddingBottom: 90 }}
- ItemSeparatorComponent={() => }
- ListFooterComponent={props.ListFooterComponent}
- renderItem={({ item }: any) => }
- />
- {props.installAction && }
-
- );
+
+
+ }
+ actions={
+
+ {/* Manual button as we don't want alert to immediately dismiss when we tap on it */}
+
+
+
+ }
+ />
+ );
+}
+
+export default function AddonPage({
+ CardComponent,
+ ...props
+}: AddonPageProps) {
+ const [search, setSearch] = React.useState("");
+ const [sortFn, setSortFn] = React.useState<((a: T, b: T) => number) | null>(
+ () => null,
+ );
+ const { bottom: bottomInset } = useSafeAreaInsets();
+ const navigation = NavigationNative.useNavigation();
+
+ useEffect(() => {
+ if (props.OptionsActionSheetComponent) {
+ navigation.setOptions({
+ headerRight: () => (
+
+ showSheet("AddonMoreSheet", props.OptionsActionSheetComponent!)
+ }
+ />
+ ),
+ });
+ }
+ }, [navigation]);
+
+ const results = useMemo(() => {
+ let values = props.items;
+ if (props.resolveItem)
+ values = values.map(props.resolveItem).filter(isNotNil);
+ const items = values.filter((i) => isNotNil(i) && typeof i === "object");
+ if (!search && sortFn) items.sort(sortFn);
+
+ return fuzzysort.go(search, items, {
+ keys: props.searchKeywords,
+ all: true,
+ });
+ }, [props.items, sortFn, search]);
+
+ const onInstallPress = useCallback(() => {
+ if (!props.installAction) return () => {};
+ const { label, onPress, fetchFn } = props.installAction;
+ if (fetchFn) {
+ openAlert(
+ "AddonInputAlert",
+ ,
+ );
+ } else {
+ onPress?.();
+ }
+ }, []);
+
+ if (results.length === 0 && !search) {
+ return (
+
+
+
+
+ Oops! Nothing to see hereโฆ yet!
+
+
+
+
+ );
+ }
+
+ const headerElement = (
+
+ {settings.safeMode?.enabled && (
+
+
+ {props.safeModeHint?.message}
+
+ {props.safeModeHint?.footer}
+
+ )}
+
+
+ setSearch(v)}
+ />
+ {props.sortOptions && (
+
+ showSimpleActionSheet({
+ key: "AddonListSortOptions",
+ header: {
+ title: "Sort Options",
+ },
+ options: Object.entries(props.sortOptions!).map(
+ ([name, fn]) => ({
+ label: name,
+ onPress: () => setSortFn(() => fn),
+ }),
+ ),
+ })
+ }
+ />
+ )}
+
+ {props.ListHeaderComponent && }
+
+ );
+
+ return (
+
+ (
+
+
+
+ Hmmm... could not find that!
+
+
+ )}
+ contentContainerStyle={{
+ padding: 8,
+ paddingHorizontal: 12,
+ paddingBottom: 90,
+ }}
+ ItemSeparatorComponent={() => }
+ ListFooterComponent={props.ListFooterComponent}
+ renderItem={({ item }: any) => (
+
+ )}
+ />
+ {props.installAction && (
+
+ )}
+
+ );
}
diff --git a/src/core/ui/reporter/components/ErrorBoundaryScreen.tsx b/src/core/ui/reporter/components/ErrorBoundaryScreen.tsx
index 29b5213..a466cb8 100644
--- a/src/core/ui/reporter/components/ErrorBoundaryScreen.tsx
+++ b/src/core/ui/reporter/components/ErrorBoundaryScreen.tsx
@@ -5,48 +5,78 @@ import { settings } from "@lib/api/settings";
import { Codeblock, ErrorBoundary } from "@lib/ui/components";
import { createStyles } from "@lib/ui/styles";
import { tokens } from "@metro/common";
-import { Button, Card, SafeAreaProvider, SafeAreaView, Text } from "@metro/common/components";
+import {
+ Button,
+ Card,
+ SafeAreaProvider,
+ SafeAreaView,
+ Text,
+} from "@metro/common/components";
import { ScrollView, View } from "react-native";
import ErrorComponentStackCard from "./ErrorComponentStackCard";
import ErrorStackCard from "./ErrorStackCard";
const useStyles = createStyles({
- container: {
- flex: 1,
- backgroundColor: tokens.colors.BG_BASE_SECONDARY,
- paddingHorizontal: 16,
- height: "100%",
- gap: 12
- }
+ container: {
+ flex: 1,
+ backgroundColor: tokens.colors.BG_BASE_SECONDARY,
+ paddingHorizontal: 16,
+ height: "100%",
+ gap: 12,
+ },
});
export default function ErrorBoundaryScreen(props: {
- error: Error;
- rerender: () => void;
+ error: Error;
+ rerender: () => void;
}) {
- const styles = useStyles();
- const debugInfo = getDebugInfo();
+ const styles = useStyles();
+ const debugInfo = getDebugInfo();
- return
-
-
-
- Uh oh.
- A crash occurred while rendering a component. This could be caused by a plugin, Kettu, or Discord itself.
- {debugInfo.os.name}; {debugInfo.discord.build} ({debugInfo.discord.version}); {debugInfo.bunny.version}
-
-
- {props.error.message}
- {hasStack(props.error) && }
- {isComponentStack(props.error) ? : null}
-
-
-
-
-
- ;
+ return (
+
+
+
+
+ Uh oh.
+
+ A crash occurred while rendering a component. This could be caused
+ by a plugin, ShiggyCord, or Discord itself.
+
+
+ {debugInfo.os.name}; {debugInfo.discord.build} (
+ {debugInfo.discord.version}); {debugInfo.bunny.version}
+
+
+
+ {props.error.message}
+ {hasStack(props.error) && }
+ {isComponentStack(props.error) ? (
+
+ ) : null}
+
+
+ BundleUpdaterManager.reload()}
+ />
+ {!settings.safeMode?.enabled && (
+ toggleSafeMode()}
+ />
+ )}
+ props.rerender()}
+ />
+
+
+
+
+ );
}
diff --git a/src/core/ui/settings/index.ts b/src/core/ui/settings/index.ts
index a671937..5ddc603 100644
--- a/src/core/ui/settings/index.ts
+++ b/src/core/ui/settings/index.ts
@@ -1,4 +1,4 @@
-import PupuIcon from "@assets/icons/kettu.png";
+import PupuIcon from "@assets/icons/shiggy.png";
import { Strings } from "@core/i18n";
import { useProxy } from "@core/vendetta/storage";
import { findAssetId } from "@lib/api/assets";
@@ -10,68 +10,62 @@ import { version } from "bunny-build-info";
export { PupuIcon };
export default function initSettings() {
-
- registerSection({
- name: "Kettu",
- items: [
- {
- key: "KETTU",
- title: () => Strings.PUPU,
- icon: { uri: PupuIcon },
- render: () => import("@core/ui/settings/pages/General"),
- useTrailing: () => `(${version})`
- },
- {
- key: "BUNNY_PLUGINS",
- title: () => Strings.PLUGINS,
- icon: findAssetId("ActivitiesIcon"),
- render: () => import("@core/ui/settings/pages/Plugins")
- },
- {
- key: "BUNNY_THEMES",
- title: () => Strings.THEMES,
- icon: findAssetId("PaintPaletteIcon"),
- render: () => import("@core/ui/settings/pages/Themes"),
- usePredicate: () => isThemeSupported()
- },
- {
- key: "BUNNY_FONTS",
- title: () => Strings.FONTS,
- icon: findAssetId("ic_add_text"),
- render: () => import("@core/ui/settings/pages/Fonts"),
- usePredicate: () => isFontSupported()
- },
- {
- key: "KETTU_BROWSER",
- title: () => Strings.BROWSER,
- icon: findAssetId("UploadIcon"),
- render: () => import("@core/ui/settings/pages/PluginBrowser"),
- },
- {
- key: "BUNNY_DEVELOPER",
- title: () => Strings.DEVELOPER,
- icon: findAssetId("WrenchIcon"),
- render: () => import("@core/ui/settings/pages/Developer"),
- usePredicate: () => useProxy(settings).developerSettings ?? false
- }
- ]
- });
+ registerSection({
+ name: "ShiggyCord",
+ items: [
+ {
+ key: "SHIGGYCORD",
+ title: () => Strings.PUPU,
+ icon: { uri: PupuIcon },
+ render: () => import("@core/ui/settings/pages/General"),
+ useTrailing: () => `(${version})`,
+ },
+ {
+ key: "BUNNY_PLUGINS",
+ title: () => Strings.PLUGINS,
+ icon: findAssetId("ActivitiesIcon"),
+ render: () => import("@core/ui/settings/pages/Plugins"),
+ },
+ {
+ key: "BUNNY_THEMES",
+ title: () => Strings.THEMES,
+ icon: findAssetId("PaintPaletteIcon"),
+ render: () => import("@core/ui/settings/pages/Themes"),
+ usePredicate: () => isThemeSupported(),
+ },
+ {
+ key: "BUNNY_FONTS",
+ title: () => Strings.FONTS,
+ icon: findAssetId("ic_add_text"),
+ render: () => import("@core/ui/settings/pages/Fonts"),
+ usePredicate: () => isFontSupported(),
+ },
- // Compat with Bunny Plugins that use configs in settings
- registerSection({
- name: "Bunny",
- items: []
- });
+ {
+ key: "BUNNY_DEVELOPER",
+ title: () => Strings.DEVELOPER,
+ icon: findAssetId("WrenchIcon"),
+ render: () => import("@core/ui/settings/pages/Developer"),
+ usePredicate: () => useProxy(settings).developerSettings ?? false,
+ },
+ ],
+ });
- // Compat with Revenge Plugins that use configs in settings
- registerSection({
- name: "Revenge",
- items: []
- });
+ // Compat with Bunny Plugins that use configs in settings
+ registerSection({
+ name: "Bunny",
+ items: [],
+ });
- // Compat with Vendetta Plugins that use configs in settings
- registerSection({
- name: "Vendetta",
- items: []
- });
-}
\ No newline at end of file
+ // Compat with Revenge Plugins that use configs in settings
+ registerSection({
+ name: "Revenge",
+ items: [],
+ });
+
+ // Compat with Vendetta Plugins that use configs in settings
+ registerSection({
+ name: "Vendetta",
+ items: [],
+ });
+}
diff --git a/src/core/ui/settings/pages/Developer/index.tsx b/src/core/ui/settings/pages/Developer/index.tsx
index 6f66a81..7efd2be 100644
--- a/src/core/ui/settings/pages/Developer/index.tsx
+++ b/src/core/ui/settings/pages/Developer/index.tsx
@@ -4,11 +4,25 @@ import AssetBrowser from "@core/ui/settings/pages/Developer/AssetBrowser";
import { useProxy } from "@core/vendetta/storage";
import { findAssetId } from "@lib/api/assets";
import { connectToDebugger } from "@lib/api/debug";
-import { getReactDevToolsProp, getReactDevToolsVersion, isLoaderConfigSupported, isReactDevToolsPreloaded, isVendettaLoader } from "@lib/api/native/loader";
+import {
+ getReactDevToolsProp,
+ getReactDevToolsVersion,
+ isLoaderConfigSupported,
+ isReactDevToolsPreloaded,
+ isVendettaLoader,
+} from "@lib/api/native/loader";
import { loaderConfig, settings } from "@lib/api/settings";
import { lazyDestructure } from "@lib/utils/lazy";
import { NavigationNative } from "@metro/common";
-import { Button, LegacyFormText, Stack, TableRow, TableRowGroup, TableSwitchRow, TextInput } from "@metro/common/components";
+import {
+ Button,
+ LegacyFormText,
+ Stack,
+ TableRow,
+ TableRowGroup,
+ TableSwitchRow,
+ TextInput,
+} from "@metro/common/components";
import { findByProps } from "@metro/wrappers";
import { semanticColors } from "@ui/color";
import { ErrorBoundary } from "@ui/components";
@@ -16,171 +30,261 @@ import { createStyles, TextStyleSheet } from "@ui/styles";
import { NativeModules } from "react-native";
import { ScrollView, StyleSheet } from "react-native";
-const { hideActionSheet } = lazyDestructure(() => findByProps("openLazy", "hideActionSheet"));
-const { showSimpleActionSheet } = lazyDestructure(() => findByProps("showSimpleActionSheet"));
-const { openAlert } = lazyDestructure(() => findByProps("openAlert", "dismissAlert"));
-const { AlertModal, AlertActionButton } = lazyDestructure(() => findByProps("AlertModal", "AlertActions"));
+const { hideActionSheet } = lazyDestructure(() =>
+ findByProps("openLazy", "hideActionSheet"),
+);
+const { showSimpleActionSheet } = lazyDestructure(() =>
+ findByProps("showSimpleActionSheet"),
+);
+const { openAlert } = lazyDestructure(() =>
+ findByProps("openAlert", "dismissAlert"),
+);
+const { AlertModal, AlertActionButton } = lazyDestructure(() =>
+ findByProps("AlertModal", "AlertActions"),
+);
const RDT_EMBED_LINK = "";
const useStyles = createStyles({
- leadingText: {
- ...TextStyleSheet["heading-md/semibold"],
- color: semanticColors.TEXT_MUTED,
- marginRight: -4
- },
+ leadingText: {
+ ...TextStyleSheet["heading-md/semibold"],
+ color: semanticColors.TEXT_MUTED,
+ marginRight: -4,
+ },
});
export default function Developer() {
- const [rdtFileExists, fs] = useFileExists("preloads/reactDevtools.js");
+ const [rdtFileExists, fs] = useFileExists("preloads/reactDevtools.js");
- const styles = useStyles();
- const navigation = NavigationNative.useNavigation();
+ const styles = useStyles();
+ const navigation = NavigationNative.useNavigation();
- useProxy(settings);
- useProxy(loaderConfig);
+ useProxy(settings);
+ useProxy(loaderConfig);
- return (
-
-
-
+ return (
+
+
+
+ (
+ ws://
+ )}
+ defaultValue={settings.debuggerUrl}
+ onChange={(v: string) => (settings.debuggerUrl = v)}
+ />
+
+ }
+ onPress={() => connectToDebugger(settings.debuggerUrl)}
+ />
+ }
+ value={!!settings.enableEvalCommand}
+ onValueChange={(v: boolean) => {
+ settings.enableEvalCommand = v;
+ }}
+ />
+
+
+ {isReactDevToolsPreloaded() && (
+
+ }
+ onPress={() =>
+ window[
+ getReactDevToolsProp() || "__vendetta_rdc"
+ ]?.connectToDevTools({
+ host: settings.debuggerUrl.split(":")?.[0],
+ resolveRNStyle: StyleSheet.flatten,
+ })
+ }
+ />
+ }
+ trailing={
+ {
+ if (rdtFileExists === CheckState.FALSE) {
+ fs.downloadFile(
+ RDT_EMBED_LINK,
+ "preloads/reactDevtools.js",
+ );
+ } else if (rdtFileExists === CheckState.TRUE) {
+ fs.removeFile("preloads/reactDevtools.js");
+ }
+ }}
+ icon={findAssetId(
+ rdtFileExists === CheckState.TRUE
+ ? "ic_message_delete"
+ : "DownloadIcon",
+ )}
+ style={{ marginLeft: 8 }}
+ />
+ }
+ />
+ {isLoaderConfigSupported() && isVendettaLoader() && (
+
+ }
+ value={loaderConfig.loadReactDevTools}
+ onValueChange={(v: boolean) => {
+ loaderConfig.loadReactDevTools = v;
+ }}
+ />
+ )}
+
+ )}
+ {isLoaderConfigSupported() && (
+
+ }
+ value={loaderConfig.customLoadUrl.enabled}
+ onValueChange={(v: boolean) => {
+ loaderConfig.customLoadUrl.enabled = v;
+ }}
+ />
+ {loaderConfig.customLoadUrl.enabled && (
+ ws://}
- defaultValue={settings.debuggerUrl}
- onChange={(v: string) => settings.debuggerUrl = v}
+ defaultValue={loaderConfig.customLoadUrl.url}
+ size="md"
+ onChange={(v: string) =>
+ (loaderConfig.customLoadUrl.url = v)
+ }
+ placeholder="http://localhost:4040/shiggycord.js"
+ label={Strings.PUPU_URL}
/>
-
- }
- onPress={() => connectToDebugger(settings.debuggerUrl)}
- />
- {isReactDevToolsPreloaded() && <>
- }
- onPress={() => window[getReactDevToolsProp() || "__vendetta_rdc"]?.connectToDevTools({
- host: settings.debuggerUrl.split(":")?.[0],
- resolveRNStyle: StyleSheet.flatten,
- })}
- />
- >}
-
- {isLoaderConfigSupported() && <>
-
- }
- value={loaderConfig.customLoadUrl.enabled}
- onValueChange={(v: boolean) => {
- loaderConfig.customLoadUrl.enabled = v;
- }}
- />
- {loaderConfig.customLoadUrl.enabled && loaderConfig.customLoadUrl.url = v}
- placeholder="http://localhost:4040/kettu.js"
- label={Strings.PUPU_URL}
- />} />}
- {isReactDevToolsPreloaded() && isVendettaLoader() && }
- value={loaderConfig.loadReactDevTools}
- onValueChange={(v: boolean) => {
- loaderConfig.loadReactDevTools = v;
- }}
- />}
-
- >}
-
-
+ )}
+
+ )}
+
+ }
+ trailing={TableRow.Arrow}
+ onPress={() =>
+ navigation.push("PUPU_CUSTOM_PAGE", {
+ title: Strings.ASSET_BROWSER,
+ render: AssetBrowser,
+ })
+ }
+ />
+ }
+ onPress={() =>
+ showSimpleActionSheet({
+ key: "ErrorBoundaryTools",
+ header: {
+ title: "Which ErrorBoundary do you want to trip?",
+ icon: (
+
+ ),
+ },
+ options: [
+ // @ts-expect-error
+ // Of course, to trigger an error, we need to do something incorrectly. The below will do!
+ {
+ label: Strings.PUPU,
+ onPress: () =>
+ navigation.push("SHIGGYCORD_CUSTOM_PAGE", {
+ render: () => ,
+ }),
+ },
+ {
+ label: "Discord",
+ isDestructive: true,
+ onPress: () =>
+ navigation.push("PUPU_CUSTOM_PAGE", {
+ noErrorBoundary: true,
+ }),
+ },
+ ],
+ })
+ }
+ />
+
- icon={}
- onPress={() => {
- openAlert("pupu-clear-bundle-reload-confirmation",
- NativeModules.BundleUpdaterManager.reload()} />
-
-
- }
- />);
- }}
+
+ }
+ onPress={() => {
+ openAlert(
+ "pupu-clear-bundle-reload-confirmation",
+
+
+ NativeModules.BundleUpdaterManager.reload()
+ }
/>
- }
- trailing={TableRow.Arrow}
- onPress={() => navigation.push("PUPU_CUSTOM_PAGE", {
- title: Strings.ASSET_BROWSER,
- render: AssetBrowser,
- })}
+
- }
- onPress={() => showSimpleActionSheet({
- key: "ErrorBoundaryTools",
- header: {
- title: "Which ErrorBoundary do you want to trip?",
- icon: ,
- onClose: () => hideActionSheet(),
- },
- options: [
- // @ts-expect-error
- // Of course, to trigger an error, we need to do something incorrectly. The below will do!
- { label: Strings.PUPU, onPress: () => navigation.push("PUPU_CUSTOM_PAGE", { render: () => }) },
- { label: "Discord", isDestructive: true, onPress: () => navigation.push("PUPU_CUSTOM_PAGE", { noErrorBoundary: true }) },
- ],
- })}
- />
- }
- trailing={
- {
- if (rdtFileExists === CheckState.FALSE) {
- fs.downloadFile(RDT_EMBED_LINK, "preloads/reactDevtools.js");
- } else if (rdtFileExists === CheckState.TRUE) {
- fs.removeFile("preloads/reactDevtools.js");
- }
- }}
- icon={findAssetId(rdtFileExists === CheckState.TRUE ? "ic_message_delete" : "DownloadIcon")}
- style={{ marginLeft: 8 }}
- />
- }
- />
- }
- value={!!settings.enableEvalCommand}
- onValueChange={(v: boolean) => {
- settings.enableEvalCommand = v;
- }}
- />
-
-
-
-
- );
-}
\ No newline at end of file
+
+ }
+ />,
+ );
+ }}
+ />
+
+
+
+
+ );
+}
diff --git a/src/core/ui/settings/pages/General/About.tsx b/src/core/ui/settings/pages/General/About.tsx
index 94f7135..409abdf 100644
--- a/src/core/ui/settings/pages/General/About.tsx
+++ b/src/core/ui/settings/pages/General/About.tsx
@@ -4,89 +4,159 @@ import Version from "@core/ui/settings/pages/General/Version";
import { useProxy } from "@core/vendetta/storage";
import { getDebugInfo } from "@lib/api/debug";
import { settings } from "@lib/api/settings";
-import { Stack, TableRowGroup } from "@metro/common/components";
-import { Platform, ScrollView } from "react-native";
+import { Stack, TableRowGroup, TableRow, Text } from "@metro/common/components";
+import { Platform, ScrollView, View } from "react-native";
+import { findAssetId } from "@lib/api/assets";
export default function About() {
- const debugInfo = getDebugInfo();
- useProxy(settings);
+ const debugInfo = getDebugInfo();
+ useProxy(settings);
- const versions = [
- {
- label: Strings.PUPU,
- version: debugInfo.bunny.version,
- icon: { uri: PupuIcon },
- },
- {
- label: "Discord",
- version: `${debugInfo.discord.version} (${debugInfo.discord.build})`,
- icon: "Discord",
- },
- {
- label: "React",
- version: debugInfo.react.version,
- icon: "ScienceIcon",
- },
- {
- label: "React Native",
- version: debugInfo.react.nativeVersion,
- icon: "MobilePhoneIcon",
- },
- {
- label: Strings.BYTECODE,
- version: debugInfo.hermes.bytecodeVersion,
- icon: "TopicsIcon",
- },
- ];
+ const coreVersions = [
+ {
+ label: Strings.PUPU,
+ version: debugInfo.bunny.version,
+ icon: { uri: PupuIcon },
+ },
+ {
+ label: "Discord",
+ version: `${debugInfo.discord.version} (${debugInfo.discord.build})`,
+ icon: "Discord",
+ },
+ {
+ label: Strings.LOADER,
+ version: `${debugInfo.bunny.loader.name} ${debugInfo.bunny.loader.version}`,
+ icon: "DownloadIcon",
+ },
+ ];
- const platformInfo = [
- {
- label: Strings.LOADER,
- version: `${debugInfo.bunny.loader.name} (${debugInfo.bunny.loader.version})`,
- icon: "DownloadIcon",
- },
- {
- label: Strings.OPERATING_SYSTEM,
- version: `${debugInfo.os.name} ${debugInfo.os.version}`,
- icon: "ScreenIcon"
- },
- ...(debugInfo.os.sdk ? [{
- label: "SDK",
+ const runtimeVersions = [
+ {
+ label: "React",
+ version: debugInfo.react.version,
+ icon: "ScienceIcon",
+ },
+ {
+ label: "React Native",
+ version: debugInfo.react.nativeVersion,
+ icon: "MobilePhoneIcon",
+ },
+ {
+ label: Strings.BYTECODE,
+ version: debugInfo.hermes.bytecodeVersion,
+ icon: "TopicsIcon",
+ },
+ ];
+
+ const deviceInfo = [
+ {
+ label: Strings.OPERATING_SYSTEM,
+ version: `${debugInfo.os.name} ${debugInfo.os.version}`,
+ icon: "ScreenIcon",
+ },
+ ...(debugInfo.os.sdk
+ ? [
+ {
+ label: "SDK Version",
version: debugInfo.os.sdk,
- icon: "StaffBadgeIcon"
- }] : []),
- {
- label: Strings.MANUFACTURER,
- version: debugInfo.device.manufacturer,
- icon: "WrenchIcon"
- },
- {
- label: Strings.BRAND,
- version: debugInfo.device.brand,
- icon: "MagicWandIcon"
- },
- {
- label: Strings.MODEL,
- version: debugInfo.device.model,
- icon: "MobilePhoneIcon"
- },
- {
- label: Platform.select({ android: Strings.CODENAME })!,
- version: debugInfo.device.codename,
- icon: "TagIcon"
- }
- ];
+ icon: "StaffBadgeIcon",
+ },
+ ]
+ : []),
+ {
+ label: "Device",
+ version: `${debugInfo.device.brand} ${debugInfo.device.model}`,
+ icon: "MobilePhoneIcon",
+ },
+ {
+ label: Strings.MANUFACTURER,
+ version: debugInfo.device.manufacturer,
+ icon: "WrenchIcon",
+ },
+ ];
- return (
-
-
-
- {versions.map(v => )}
-
-
- {platformInfo.map(p => )}
-
-
-
- );
+ return (
+
+
+
+
+ {Strings.PUPU}
+
+
+ Shiggy your discord client!
+
+
+
+
+ {coreVersions.map((v, i) => (
+
+ ))}
+
+
+
+ {runtimeVersions.map((v, i) => (
+
+ ))}
+
+
+
+ {deviceInfo.map((p, i) => (
+
+ ))}
+
+
+
+
+ }
+ icon={}
+ />
+
+ }
+ icon={}
+ />
+
+ }
+ icon={}
+ />
+
+
+
+ );
}
diff --git a/src/core/ui/settings/pages/General/index.tsx b/src/core/ui/settings/pages/General/index.tsx
index f7f6071..75d29d0 100644
--- a/src/core/ui/settings/pages/General/index.tsx
+++ b/src/core/ui/settings/pages/General/index.tsx
@@ -8,113 +8,174 @@ import { getDebugInfo } from "@lib/api/debug";
import { BundleUpdaterManager } from "@lib/api/native/modules";
import { settings } from "@lib/api/settings";
import { openAlert } from "@lib/ui/alerts";
-import { DISCORD_SERVER, GITHUB, CODEBERG } from "@lib/utils/constants";
+import { DISCORD_SERVER, GITHUB } from "@lib/utils/constants";
import { NavigationNative } from "@metro/common";
-import { AlertActionButton, AlertActions, AlertModal, Stack, TableRow, TableRowGroup, TableSwitchRow } from "@metro/common/components";
+import {
+ AlertActionButton,
+ AlertActions,
+ AlertModal,
+ Stack,
+ TableRow,
+ TableRowGroup,
+ TableSwitchRow,
+} from "@metro/common/components";
import { Linking, ScrollView } from "react-native";
import CodebergIcon from "@assets/icons/codeberg-logo_icon_white.png";
export default function General() {
- useProxy(settings);
+ useProxy(settings);
- const debugInfo = getDebugInfo();
- const navigation = NavigationNative.useNavigation();
+ const debugInfo = getDebugInfo();
+ const navigation = NavigationNative.useNavigation();
- return (
-
-
-
- }
- trailing={}
- />
- }
- trailing={}
- />
- }
- onPress={() => navigation.push("PUPU_CUSTOM_PAGE", {
- title: Strings.ABOUT,
- render: () => ,
- })}
- />
-
-
- }
- onPress={() => Linking.openURL(DISCORD_SERVER)}
- />
- }
- onPress={() => Linking.openURL(CODEBERG)}
- />
- }
- onPress={() => Linking.openURL(GITHUB)}
- />
-
-
- }
+ return (
+
+
+
+ }
+ trailing={
+
+ }
+ />
+ }
+ trailing={
+
+ }
+ />
+ }
+ trailing={
+
+ }
+ />
+ }
+ trailing={}
+ />
+
+
+ }
+ onPress={() => BundleUpdaterManager.reload()}
+ />
+ }
+ value={isSafeMode()}
+ onValueChange={(to: boolean) => {
+ toggleSafeMode({ to, reload: false });
+ openAlert(
+ "bunny-reload-safe-mode",
+
+ BundleUpdaterManager.reload()}
- />
- }
- value={isSafeMode()}
- onValueChange={(to: boolean) => {
- toggleSafeMode({ to, reload: false });
- openAlert(
- "bunny-reload-safe-mode",
-
- BundleUpdaterManager.reload()}
- />
-
- }
- />
- );
- }}
- />
- }
- value={settings.developerSettings}
- onValueChange={(v: boolean) => {
- settings.developerSettings = v;
- }}
- />
-
-
- }
- value={settings.enableDiscordDeveloperSettings}
- onValueChange={(v: boolean) => {
- settings.enableDiscordDeveloperSettings = v;
- }}
- />
-
-
-
- );
+ />
+
+
+ }
+ />,
+ );
+ }}
+ />
+
+
+ }
+ value={settings.developerSettings}
+ onValueChange={(v: boolean) => {
+ settings.developerSettings = v;
+ }}
+ />
+ }
+ value={settings.enableDiscordDeveloperSettings}
+ onValueChange={(v: boolean) => {
+ settings.enableDiscordDeveloperSettings = v;
+ }}
+ />
+
+
+ }
+ onPress={() => Linking.openURL(DISCORD_SERVER)}
+ />
+
+ }
+ onPress={() => Linking.openURL(GITHUB)}
+ />
+
+
+
+ }
+ onPress={() =>
+ navigation.push("PUPU_CUSTOM_PAGE", {
+ title: Strings.ABOUT,
+ render: () => ,
+ })
+ }
+ />
+
+
+
+ );
}
diff --git a/src/core/ui/settings/pages/PluginBrowser/index.tsx b/src/core/ui/settings/pages/PluginBrowser/index.tsx
deleted file mode 100644
index 4bf41af..0000000
--- a/src/core/ui/settings/pages/PluginBrowser/index.tsx
+++ /dev/null
@@ -1,499 +0,0 @@
-import { React, NavigationNative } from "@metro/common";
-import { View } from "react-native";
-import { Stack, Button, IconButton, Text, Card, FlashList } from "@metro/common/components";
-import { findAssetId } from "@lib/api/assets";
-import safeFetch from "@lib/utils/safeFetch";
-import { showToast } from "@ui/toasts";
-import Search from "@ui/components/Search";
-import { VdPluginManager } from "@core/vendetta/plugins";
-import { installTheme, themes, removeTheme } from "@lib/addons/themes";
-import { clipboard } from "@metro/common";
-import { hideSheet, showSheet } from "@lib/ui/sheets";
-import { AlertActionButton } from "@lib/ui/components/wrappers";
-import { dismissAlert, openAlert } from "@lib/ui/alerts";
-import { ActionSheet, AlertModal, AlertActions, TableRow, TableRowGroup } from "@metro/common/components";
-import { lazyDestructure } from "@lib/utils/lazy";
-import { findByProps } from "@metro";
-
-const { showSimpleActionSheet, hideActionSheet } = lazyDestructure(() => findByProps("showSimpleActionSheet"));
-
-interface BaseAddonData {
- name: string;
- description: string;
- authors: string[];
- installUrl: string;
-}
-
-interface PluginData extends BaseAddonData {
- status: "working" | "broken" | "warning" | string;
- sourceUrl: string;
- warningMessage?: string;
-}
-
-interface ThemeData extends BaseAddonData {}
-
-type AddonData = PluginData | ThemeData;
-
-const PLUGIN_URL = "https://raw.githubusercontent.com/Purple-EyeZ/Plugins-List/refs/heads/main/src/plugins-data.json";
-const THEME_URL = "https://raw.githubusercontent.com/kmmiio99o/theme-marketplace/refs/heads/main/themes.json";
-
-function normalizeIdFromInstallUrl(url: string) {
- return url.endsWith("/") ? url : url + "/";
-}
-
-// @ts-ignore (i cant be bothered to type these)
-function InstallButton({ addon, isPluginMode, installing, setInstalling, setRefreshTick }) {
- const normId = normalizeIdFromInstallUrl(addon.installUrl);
- const [installed, setInstalled] = React.useState(() =>
- isPluginMode ? Boolean(VdPluginManager.plugins[normId]) : Boolean(themes[addon.installUrl])
- );
-
- React.useEffect(() => {
- setInstalled(isPluginMode ? Boolean(VdPluginManager.plugins[normId]) : Boolean(themes[addon.installUrl]));
- }, [addon.installUrl, setRefreshTick, isPluginMode]);
-
- const installAddon = async () => {
- if (installing.has(normId)) return;
- setInstalling((prev: Iterable | null | undefined) => new Set(prev).add(normId));
- try {
- if (isPluginMode) {
- await VdPluginManager.installPlugin(normId, true);
- } else {
- await installTheme(addon.installUrl);
- }
- showToast(`Installed ${addon.name}`, findAssetId("CheckIcon"));
- setInstalled(true);
- } catch (e) {
- showToast(e instanceof Error ? e.message : String(e), findAssetId("CircleXIcon-primary"));
- } finally {
- setInstalling((prev: Iterable | null | undefined) => { const s = new Set(prev); s.delete(normId); return s; });
- setRefreshTick((t: number) => t + 1);
- }
- };
-
- const uninstallAddon = async () => {
- try {
- if (isPluginMode) {
- await VdPluginManager.removePlugin(normId);
- } else {
- await removeTheme(addon.installUrl);
- }
- showToast(`Uninstalled ${addon.name}`, findAssetId("TrashIcon"));
- setInstalled(false);
- } catch (e) {
- showToast(e instanceof Error ? e.message : String(e), findAssetId("CircleXIcon-primary"));
- } finally {
- setRefreshTick((t: number) => t + 1);
- }
- };
-
- const promptInstall = () => {
- if (!isPluginMode) return installAddon();
-
- const plugin = addon as PluginData;
- const needsWarn = (plugin.status && plugin.status !== "working") || (plugin.warningMessage && plugin.warningMessage.trim().length > 0);
- if (!needsWarn) return installAddon();
-
- const lines: string[] = [];
- if (plugin.status && plugin.status !== "working") {
- if (plugin.status === "broken") lines.push("This plugin is marked as broken, please be aware you may encounter issues");
- else if (plugin.status === "warning") lines.push("This plugin may have issues");
- else lines.push(`Status: ${plugin.status}`);
- }
- if (plugin.warningMessage) lines.push(plugin.warningMessage);
-
- openAlert("plugins-list-install-warning", (
- {lines.join("\n\n")}}
- actions={
- { dismissAlert("plugins-list-install-warning"); installAddon(); }}
- />
- dismissAlert("plugins-list-install-warning")}
- />
- }
- />
- ));
- };
-
- return (
-
- );
-}
-
-// @ts-ignore (i cant be bothered to type these)
-function TrailingButtons({ addon, isPluginMode, installing, setInstalling, setRefreshTick }) {
- const copyAddonLink = () => {
- clipboard.setString(addon.installUrl);
- // @ts-ignore
- showToast.showCopyToClipboard?.();
- };
-
- const copySourceUrl = () => {
- const plugin = addon as PluginData;
- clipboard.setString(plugin.sourceUrl);
- // @ts-ignore
- showToast.showCopyToClipboard?.();
- };
-
- const openAddonMenu = () => {
- const actions = [
- {
- label: `Copy ${isPluginMode ? 'Plugin' : 'Theme'} Link`,
- icon: findAssetId("CopyIcon"),
- onPress: copyAddonLink
- }
- ];
-
- if (isPluginMode && (addon as PluginData).sourceUrl) {
- actions.push({
- label: "Copy Source URL",
- icon: findAssetId("CopyIcon"),
- onPress: copySourceUrl
- });
- }
-
- const sheetKey = `${isPluginMode ? 'plugin' : 'theme'}-menu`;
- showSheet(sheetKey, () => (
-
-
- {actions.map((action, index) => (
- }
- onPress={() => {
- action.onPress();
- hideSheet(sheetKey);
- }}
- />
- ))}
-
-
- ));
- };
-
- return (
-
-
-
-
- );
-}
-
-// @ts-ignore (i cant be bothered to type these)
-function AddonCard({ addon, isPluginMode, installing, setInstalling, setRefreshTick }) {
- const { name, description, authors } = addon;
- const plugin = addon as PluginData;
-
- let statusColor = "text-normal";
- if (isPluginMode) {
- if (plugin.status === "working") statusColor = "#4ADE80";
- if (plugin.status === "broken") statusColor = "#EF4444";
- if (plugin.status === "warning") statusColor = "#F59E0B";
- }
-
- return (
-
-
-
-
-
- {name}
-
-
- by {authors?.join(", ") || "Unknown"}
-
- {isPluginMode && (
-
- Status: {plugin.status}
-
- )}
-
-
-
-
-
-
- {description}
-
- {isPluginMode && plugin.warningMessage && (
-
- Warning: {plugin.warningMessage}
-
- )}
-
-
- );
-}
-
-enum Sort {
- DateNewest = "Newest",
- DateOldest = "Oldest",
- NameAZ = "Name (AโZ)",
- NameZA = "Name (ZโA)",
- WorkingFirst = "Working First",
- BrokenFirst = "Broken First",
-}
-
-export default function BrowserPage() {
- const navigation = NavigationNative.useNavigation();
-
- const [mode, setMode] = React.useState<"plugins" | "themes">("plugins");
- const [plugins, setPlugins] = React.useState([]);
- const [themesList, setThemesList] = React.useState([]);
- const [loading, setLoading] = React.useState(true);
- const [error, setError] = React.useState(null);
- const [searchQuery, setSearchQuery] = React.useState("");
- const [installing, setInstalling] = React.useState>(new Set());
- const [refreshTick, setRefreshTick] = React.useState(0);
- const [sort, setSort] = React.useState(Sort.DateNewest);
-
- React.useEffect(() => {
- navigation.setOptions({
- title: "Browser"
- });
- }, [navigation]);
-
- const fetchData = React.useCallback(async (isPluginMode: boolean) => {
- setLoading(true);
- setError(null);
- try {
- const url = isPluginMode ? PLUGIN_URL : THEME_URL;
- const response = await safeFetch(url);
- if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
- const data = await response.json();
-
- let addonList: AddonData[] = [];
- if (Array.isArray(data)) {
- addonList = data;
- } else if (isPluginMode && data.OFFICIAL_PLUGINS) {
- addonList = data.OFFICIAL_PLUGINS;
- } else if (!isPluginMode) {
- // Handle any other structure for themes - try common property names
- addonList = data.OFFICIAL_THEMES || data.themes || data.THEMES || data.items || [];
- }
-
- if (isPluginMode) {
- setPlugins(addonList as PluginData[]);
- } else {
- setThemesList(addonList as ThemeData[]);
- }
- } catch (e) {
- setError(e instanceof Error ? e.message : String(e));
- if (isPluginMode) {
- setPlugins([]);
- } else {
- setThemesList([]);
- }
- } finally {
- setLoading(false);
- }
- }, []);
-
- const fetchPlugins = React.useCallback(() => fetchData(true), [fetchData]);
- const fetchThemes = React.useCallback(() => fetchData(false), [fetchData]);
-
- React.useEffect(() => {
- fetchPlugins();
- fetchThemes();
- }, [fetchPlugins, fetchThemes]);
-
- const filterList = (list: AddonData[]) => {
- if (!list) return [] as AddonData[];
- const q = searchQuery.toLowerCase();
- if (!q) return list;
- return list.filter(p =>
- p.name.toLowerCase().includes(q)
- || p.description.toLowerCase().includes(q)
- || (p.authors || []).some(a => a.toLowerCase().includes(q))
- );
- };
-
- const sortedAndFiltered = React.useMemo(() => {
- const list = filterList(mode === "plugins" ? plugins : themesList);
-
- const getStatusPriority = (status: PluginData["status"], sortBy: Sort): number => {
- if (sortBy === Sort.WorkingFirst) {
- return status === "working" || status === "warning" ? 0 : 1;
- }
- if (sortBy === Sort.BrokenFirst) {
- return status === "broken" ? 0 : 1;
- }
- return 0;
- };
-
- switch (sort) {
- case Sort.DateNewest:
- return [...list].reverse();
- case Sort.DateOldest:
- return [...list];
- case Sort.NameAZ:
- return [...list].sort((a, b) => a.name.localeCompare(b.name));
- case Sort.NameZA:
- return [...list].sort((a, b) => b.name.localeCompare(a.name));
- case Sort.WorkingFirst:
- if (mode === "plugins") {
- return [...list].sort((a, b) => {
- const pa = getStatusPriority((a as PluginData).status, Sort.WorkingFirst);
- const pb = getStatusPriority((b as PluginData).status, Sort.WorkingFirst);
- return pa !== pb ? pa - pb : a.name.localeCompare(b.name);
- });
- }
- return list;
- case Sort.BrokenFirst:
- if (mode === "plugins") {
- return [...list].sort((a, b) => {
- const pa = getStatusPriority((a as PluginData).status, Sort.BrokenFirst);
- const pb = getStatusPriority((b as PluginData).status, Sort.BrokenFirst);
- return pa !== pb ? pa - pb : a.name.localeCompare(b.name);
- });
- }
- return list;
- default:
- return list;
- }
- }, [plugins, themesList, mode, searchQuery, sort]);
-
- if (error) {
- return (
-
-
-
- An error occurred while fetching the repository
-
-
- {error}
-
- fetchData(mode === "plugins")}
- icon={findAssetId("RetryIcon")}
- />
-
-
- );
- }
-
- return (
-
-
-
-
-
- setMode("plugins")}
- />
- setMode("themes")}
- />
-
-
-
-
-
-
- showSimpleActionSheet({
- key: "AddonListSortOptions",
- header: {
- title: "Sort Options",
- onClose: () => hideActionSheet("AddonListSortOptions"),
- },
- options: Object.entries(Sort).map(([key, value]) => ({
- label: value,
- onPress: () => {
- setSort(value as Sort);
- }
- }))
- })}
- />
-
-
-
-
-
-
-
-
-
-
- Unproxied Plugins
-
- Plugins installed from this source have not been checked for safety, install at your own risk
-
-
-
-
-
- ) : null}
- //@ts-ignore
- renderItem={({ item: addon }) => (
-
-
-
- )}
- />
-
- );
-}
\ No newline at end of file
diff --git a/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx b/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx
index b199f55..6ab8f1e 100644
--- a/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx
+++ b/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx
@@ -3,154 +3,182 @@ import { UnifiedPluginModel } from "@core/ui/settings/pages/Plugins/models";
import { usePluginCardStyles } from "@core/ui/settings/pages/Plugins/usePluginCardStyles";
import { findAssetId } from "@lib/api/assets";
import { NavigationNative, tokens } from "@metro/common";
-import { Card, IconButton, Stack, TableSwitch, Text } from "@metro/common/components";
+import {
+ Card,
+ IconButton,
+ Stack,
+ TableSwitch,
+ Text,
+} from "@metro/common/components";
import { showSheet } from "@ui/sheets";
import chroma from "chroma-js";
import { createContext, useContext, useMemo } from "react";
import { Image, View } from "react-native";
-const CardContext = createContext<{ plugin: UnifiedPluginModel, result: Fuzzysort.KeysResult; }>(null!);
+const CardContext = createContext<{
+ plugin: UnifiedPluginModel;
+ result: Fuzzysort.KeysResult;
+}>(null!);
const useCardContext = () => useContext(CardContext);
function getHighlightColor(): import("react-native").ColorValue {
- return chroma(tokens.unsafe_rawColors.YELLOW_300).alpha(0.3).hex();
+ return chroma(tokens.unsafe_rawColors.YELLOW_300).alpha(0.3).hex();
}
function Title() {
- const styles = usePluginCardStyles();
- const { plugin, result } = useCardContext();
+ const styles = usePluginCardStyles();
+ const { plugin, result } = useCardContext();
- // could be empty if the plugin name is irrelevant!
- const highlightedNode = result[0].highlight((m, i) =>
-
- {m}
-
- );
+ // could be empty if the plugin name is irrelevant!
+ const highlightedNode = result[0].highlight((m, i) => (
+
+ {m}
+
+ ));
- const icon = plugin.icon && findAssetId(plugin.icon);
+ const icon = plugin.icon && findAssetId(plugin.icon);
- const textNode = (
-
- {highlightedNode.length ? highlightedNode : plugin.name}
-
- );
+ const textNode = (
+
+ {highlightedNode.length ? highlightedNode : plugin.name}
+
+ );
- return
- {icon && }
- {textNode}
- ;
+ return (
+
+ {icon && }
+ {textNode}
+
+ );
}
function Authors() {
- const { plugin, result } = useCardContext();
- const styles = usePluginCardStyles();
+ const { plugin, result } = useCardContext();
+ const styles = usePluginCardStyles();
- if (!plugin.authors) return null;
+ if (!plugin.authors) return null;
- // could be empty if the author(s) are irrelevant with the search!
- const highlightedNode = result[2].highlight((m, i) =>
-
- {m}
-
- );
+ // could be empty if the author(s) are irrelevant with the search!
+ const highlightedNode = result[2].highlight((m, i) => (
+
+ {m}
+
+ ));
- const badges = plugin.getBadges();
- const authorText = highlightedNode.length > 0 ? highlightedNode : plugin.authors.map(a => a.name).join(", ");
+ const badges = plugin.getBadges();
+ const authorText =
+ highlightedNode.length > 0
+ ? highlightedNode
+ : plugin.authors.map((a) => a.name).join(", ");
- return (
-
-
- by {authorText}
-
- {badges.length > 0 &&
- {badges.map((b, i) => )}
- }
+ return (
+
+
+ by {authorText}
+
+ {badges.length > 0 && (
+
+ {badges.map((b, i) => (
+
+ ))}
- );
+ )}
+
+ );
}
function Description() {
- const { plugin, result } = useCardContext();
+ const { plugin, result } = useCardContext();
- // could be empty if the description is irrelevant with the search!
- const highlightedNode = result[1].highlight((m, i) =>
- {m}
- );
+ // could be empty if the description is irrelevant with the search!
+ const highlightedNode = result[1].highlight((m, i) => (
+
+ {m}
+
+ ));
- return
- {highlightedNode.length ? highlightedNode : plugin.description}
- ;
+ return (
+
+ {highlightedNode.length ? highlightedNode : plugin.description}
+
+ );
}
const Actions = () => {
- const { plugin } = useCardContext();
- const navigation = NavigationNative.useNavigation();
+ const { plugin } = useCardContext();
+ const navigation = NavigationNative.useNavigation();
- return
- navigation.push("PUPU_CUSTOM_PAGE", {
- title: plugin.name,
- render: plugin.getPluginSettingsComponent(),
- })}
- />
- void showSheet(
- "PluginInfoActionSheet",
- plugin.resolveSheetComponent(),
- { plugin, navigation }
- )}
- />
- ;
+ return (
+
+
+ navigation.push("PUPU_CUSTOM_PAGE", {
+ title: plugin.name,
+ render: plugin.getPluginSettingsComponent(),
+ })
+ }
+ />
+
+ void showSheet(
+ "PluginInfoActionSheet",
+ plugin.resolveSheetComponent(),
+ { plugin, navigation },
+ )
+ }
+ />
+
+ );
};
-export default function PluginCard({ result, item: plugin }: CardWrapper) {
- plugin.usePluginState();
+export default function PluginCard({
+ result,
+ item: plugin,
+}: CardWrapper) {
+ plugin.usePluginState();
- const [, forceUpdate] = React.useReducer(() => ({}), 0);
- const cardContextValue = useMemo(() => ({ plugin, result }), [plugin, result]);
+ const [, forceUpdate] = React.useReducer(() => ({}), 0);
+ const cardContextValue = useMemo(
+ () => ({ plugin, result }),
+ [plugin, result],
+ );
- return (
-
-
-
-
-
-
-
-
-
-
-
- {
- plugin.toggle(v);
- forceUpdate();
- }}
- />
-
-
-
-
-
-
-
- );
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {
+ plugin.toggle(v);
+ forceUpdate();
+ }}
+ />
+
+
+
+
+
+
+
+ );
}
diff --git a/src/core/ui/settings/pages/Plugins/index.tsx b/src/core/ui/settings/pages/Plugins/index.tsx
index 9c5405c..5a01e56 100644
--- a/src/core/ui/settings/pages/Plugins/index.tsx
+++ b/src/core/ui/settings/pages/Plugins/index.tsx
@@ -1,9 +1,26 @@
+import { React, NavigationNative } from "@metro/common";
+const { useReducer } = React;
+import { View, Image } from "react-native";
+import {
+ Stack,
+ Button,
+ IconButton,
+ Text,
+ Card,
+ FlashList,
+} from "@metro/common/components";
import { Strings } from "@core/i18n";
import AddonPage from "@core/ui/components/AddonPage";
import PluginCard from "@core/ui/settings/pages/Plugins/components/PluginCard";
import { VdPluginManager } from "@core/vendetta/plugins";
import { useProxy } from "@core/vendetta/storage";
-import { isCorePlugin, isPluginInstalled, pluginSettings, registeredPlugins } from "@lib/addons/plugins";
+import {
+ corePluginInstances,
+ isCorePlugin,
+ isPluginInstalled,
+ pluginSettings,
+ registeredPlugins,
+} from "@lib/addons/plugins";
import { Author } from "@lib/addons/types";
import { findAssetId } from "@lib/api/assets";
import { settings } from "@lib/api/settings";
@@ -12,125 +29,539 @@ import { showToast } from "@lib/ui/toasts";
import { BUNNY_PROXY_PREFIX, VD_PROXY_PREFIX } from "@lib/utils/constants";
import { lazyDestructure } from "@lib/utils/lazy";
import { findByProps } from "@metro";
-import { NavigationNative } from "@metro/common";
-import { Button, Card, FlashList, IconButton, Text } from "@metro/common/components";
import { ComponentProps } from "react";
-import { View } from "react-native";
+import safeFetch from "@lib/utils/safeFetch";
+import Search from "@ui/components/Search";
+import { clipboard } from "@metro/common";
+import {
+ ActionSheet,
+ AlertModal,
+ AlertActions,
+ TableRow,
+ TableRowGroup,
+} from "@metro/common/components";
import { UnifiedPluginModel } from "./models";
import unifyBunnyPlugin from "./models/bunny";
import unifyVdPlugin from "./models/vendetta";
-const { openAlert } = lazyDestructure(() => findByProps("openAlert", "dismissAlert"));
-const { AlertModal, AlertActions, AlertActionButton } = lazyDestructure(() => findByProps("AlertModal", "AlertActions"));
+const { openAlert } = lazyDestructure(() =>
+ findByProps("openAlert", "dismissAlert"),
+);
+const {
+ AlertModal: AlertModalComponent,
+ AlertActions: AlertActionsComponent,
+ AlertActionButton,
+} = lazyDestructure(() => findByProps("AlertModal", "AlertActions"));
+const { showSimpleActionSheet, hideActionSheet } = lazyDestructure(() =>
+ findByProps("showSimpleActionSheet"),
+);
-interface PluginPageProps extends Partial>> {
- useItems: () => unknown[];
+interface BaseAddonData {
+ name: string;
+ description: string;
+ authors: string[];
+ installUrl: string;
}
-function PluginPage(props: PluginPageProps) {
- const items = props.useItems();
+interface PluginData extends BaseAddonData {
+ status: "working" | "broken" | "warning" | string;
+ sourceUrl: string;
+ warningMessage?: string;
+}
- return
- CardComponent={PluginCard}
- title={Strings.PLUGINS}
- searchKeywords={[
- "name",
- "description",
- p => p.authors?.map(
- (a: Author | string) => typeof a === "string" ? a : a.name
- ).join() || ""
- ]}
- sortOptions={{
- "Name (A-Z)": (a, b) => a.name.localeCompare(b.name),
- "Name (Z-A)": (a, b) => b.name.localeCompare(a.name)
+type AddonData = PluginData;
+
+const PLUGIN_URL =
+ "https://raw.githubusercontent.com/Purple-EyeZ/Plugins-List/refs/heads/main/src/plugins-data.json";
+
+enum Sort {
+ DateNewest = "Date Added (Newest First)",
+ DateOldest = "Date Added (Oldest First)",
+ NameAZ = "Name (A-Z)",
+ NameZA = "Name (Z-A)",
+ WorkingFirst = "Working First",
+ BrokenFirst = "Broken First",
+}
+
+function normalizeIdFromInstallUrl(url: string) {
+ return url.endsWith("/") ? url : url + "/";
+}
+
+function InstallButton({
+ addon,
+ installing,
+ setInstalling,
+ setRefreshTick,
+}: any) {
+ const pluginId = normalizeIdFromInstallUrl(addon.installUrl);
+ const plugin = VdPluginManager.plugins[pluginId];
+ const installed = !!plugin;
+ const isInstalling = installing.has(pluginId);
+
+ // Determine if this is the QuickInstall plugin which can't be disabled
+ const isQuickInstall = pluginId.toLowerCase().includes("quickinstall");
+ const [, forceUpdate] = useReducer((x) => x + 1, 0);
+ const [rotation, setRotation] = React.useState(0);
+ const [pulse, setPulse] = React.useState(1);
+
+ const handleInstall = async () => {
+ if (isInstalling) return;
+
+ // Check for broken or warning status
+ if (addon.status === "broken" || addon.status === "warning") {
+ const isBroken = addon.status === "broken";
+ openAlert(
+ "bunny-plugin-status-warning",
+
+ {addon.warningMessage}
+
+ ) : null
+ }
+ actions={
+
+
+
+
+ }
+ />,
+ );
+ return;
+ }
+
+ continueInstallation();
+ };
+
+ const performInstall = async () => {
+ setInstalling((prev: Set) => new Set([...prev, pluginId]));
+
+ // Start animation
+ const animationInterval = setInterval(() => {
+ setRotation((prev) => (prev + 15) % 360);
+ setPulse((prev) => 0.8 + 0.2 * Math.sin(Date.now() / 200));
+ forceUpdate();
+ }, 100);
+
+ try {
+ await VdPluginManager.installPlugin(addon.installUrl);
+ setRefreshTick((prev: number) => prev + 1);
+ } catch (e) {
+ openAlert(
+ "bunny-plugin-install-failed",
+
+
+ {e instanceof Error ? e.message : String(e)}
+
+
+ }
+ actions={}
+ />,
+ );
+ } finally {
+ clearInterval(animationInterval);
+ setRotation(0);
+ setPulse(1);
+ setInstalling((prev: Set) => {
+ const newSet = new Set(prev);
+ newSet.delete(pluginId);
+ return newSet;
+ });
+ }
+ };
+
+ const continueInstallation = async () => {
+ if (
+ !addon.installUrl.startsWith(VD_PROXY_PREFIX) &&
+ !addon.installUrl.startsWith(BUNNY_PROXY_PREFIX) &&
+ !settings.developerSettings
+ ) {
+ openAlert(
+ "bunny-plugin-unproxied-confirmation",
+
+ {addon.installUrl}
+
+ }
+ actions={
+
+
+
+
+ }
+ />,
+ );
+ } else {
+ performInstall();
+ }
+ };
+
+ const handleUninstall = () => {
+ VdPluginManager.removePlugin(pluginId);
+ setRefreshTick((prev: number) => prev + 1);
+ };
+
+ if (installed) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+ ) : (
+ findAssetId("DownloadIcon")
+ )
+ }
+ onPress={handleInstall}
+ />
+
+ );
+}
+
+function TrailingButtons({
+ addon,
+ installing,
+ setInstalling,
+ setRefreshTick,
+}: any) {
+ return (
+
+ {
+ showSimpleActionSheet({
+ key: "AddonShareSheet",
+ header: {
+ title: "Share Plugin",
+ },
+ options: [
+ {
+ label: "Copy Install URL",
+ onPress: () => {
+ clipboard.setString(addon.installUrl);
+ // URL copied
+ },
+ },
+ {
+ label: "Copy Source URL",
+ onPress: () => {
+ if (addon.sourceUrl) {
+ clipboard.setString(addon.sourceUrl);
+ // Source URL copied
+ }
+ },
+ },
+ ],
+ });
}}
- safeModeHint={{ message: Strings.SAFE_MODE_NOTICE_PLUGINS }}
- items={items}
- {...props}
- />;
+ />
+
+
+ );
+}
+
+function BrowseAddonCard({
+ addon,
+ installing,
+ setInstalling,
+ setRefreshTick,
+}: any) {
+ const { name, description, authors, status, warningMessage } = addon;
+
+ let statusColor = "text-normal";
+ if (status === "working") statusColor = "#4ADE80";
+ if (status === "broken") statusColor = "#EF4444";
+ if (status === "warning") statusColor = "#F59E0B";
+
+ return (
+
+
+
+
+
+ {name}
+
+
+ by {authors?.join(", ") || "Unknown"}
+
+
+ Status: {status}
+
+
+
+
+
+
+ {description}
+ {warningMessage && (
+
+ Warning: {warningMessage}
+
+ )}
+
+
+ );
+}
+
+interface PluginPageProps
+ extends Partial>> {
+ useItems: () => unknown[];
+}
+
+function InstalledPluginPage(props: PluginPageProps) {
+ const items = props.useItems();
+
+ return (
+
+ CardComponent={PluginCard}
+ title={Strings.PLUGINS}
+ searchKeywords={[
+ "name",
+ "description",
+ (p) =>
+ p.authors
+ ?.map((a: Author | string) => (typeof a === "string" ? a : a.name))
+ .join() || "",
+ ]}
+ sortOptions={{
+ "Name (A-Z)": (a, b) => a.name.localeCompare(b.name),
+ "Name (Z-A)": (a, b) => b.name.localeCompare(a.name),
+ }}
+ safeModeHint={{ message: Strings.SAFE_MODE_NOTICE_PLUGINS }}
+ items={items}
+ {...props}
+ />
+ );
+}
+
+interface BrowsePluginPageProps {
+ plugins: PluginData[];
+ installing: Set;
+ setInstalling: React.Dispatch>>;
+ setRefreshTick: React.Dispatch>;
+}
+
+function BrowsePluginPage(props: BrowsePluginPageProps) {
+ return (
+
+ CardComponent={({ item }) => (
+
+ )}
+ title={Strings.PLUGINS}
+ searchKeywords={[
+ "name",
+ "description",
+ (p) => (p.authors || []).join(", "),
+ ]}
+ sortOptions={{
+ "Date (Newest)": (a, b) => 0,
+ "Date (Oldest)": (a, b) => 0,
+ "Name (A-Z)": (a, b) => a.name.localeCompare(b.name),
+ "Name (Z-A)": (a, b) => b.name.localeCompare(a.name),
+ }}
+ items={props.plugins}
+ />
+ );
}
export default function Plugins() {
- useProxy(settings);
- const navigation = NavigationNative.useNavigation();
+ useProxy(settings);
+ const navigation = NavigationNative.useNavigation();
- return ("installed");
+ const [plugins, setPlugins] = React.useState([]);
+ const [loading, setLoading] = React.useState(false);
+ const [error, setError] = React.useState(null);
+ const [installing, setInstalling] = React.useState>(new Set());
+ const [refreshTick, setRefreshTick] = React.useState(0);
+
+ React.useEffect(() => {
+ navigation.setOptions({
+ headerRight: () => (
+ setMode(mode === "installed" ? "browse" : "installed")}
+ />
+ ),
+ });
+ }, [navigation, mode]);
+
+ const fetchPlugins = React.useCallback(async () => {
+ setLoading(true);
+ setError(null);
+ try {
+ const response = await safeFetch(PLUGIN_URL);
+ if (!response.ok)
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+ const data = await response.json();
+
+ let pluginList: PluginData[] = [];
+ if (Array.isArray(data)) {
+ pluginList = data;
+ } else if (data.OFFICIAL_PLUGINS) {
+ pluginList = data.OFFICIAL_PLUGINS;
+ }
+
+ setPlugins(pluginList);
+ } catch (e) {
+ setError(e instanceof Error ? e.message : String(e));
+ setPlugins([]);
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ React.useEffect(() => {
+ if (mode === "browse") {
+ fetchPlugins();
+ }
+ }, [mode, fetchPlugins, refreshTick]);
+
+ if (mode === "installed") {
+ return (
+ {
- useProxy(VdPluginManager.plugins);
- useObservable([pluginSettings]);
+ useProxy(VdPluginManager.plugins);
+ useObservable([pluginSettings]);
- const vdPlugins = Object.values(VdPluginManager.plugins).map(unifyVdPlugin);
- const bnPlugins = [...registeredPlugins.values()].filter(p => isPluginInstalled(p.id) && !isCorePlugin(p.id)).map(unifyBunnyPlugin);
+ const vdPlugins = Object.values(VdPluginManager.plugins).map(
+ unifyVdPlugin,
+ );
- return [...vdPlugins, ...bnPlugins];
- }}
- ListHeaderComponent={() => {
- const unproxiedPlugins = Object.values(VdPluginManager.plugins).filter(p => !p.id.startsWith(VD_PROXY_PREFIX) && !p.id.startsWith(BUNNY_PROXY_PREFIX));
- if (!unproxiedPlugins.length) return null;
-
- return
-