From a5e81e4265f46ce9ebc8a6b94ccf4f0253b5edfe Mon Sep 17 00:00:00 2001 From: Idrees Hassan Date: Sun, 16 Nov 2025 09:25:48 -0500 Subject: [PATCH] Allow for set contexts --- build.js | 18 + dist/birb.js | 22 +- dist/extension.zip | Bin 149911 -> 150059 bytes dist/extension/birb.js | 22 +- dist/obsidian/main.js | 22 +- dist/userscript/birb.user.js | 22 +- dist/vencord/birb.export.js | 2837 ++++++++++++++++++++++++++ platform-specific/vencord/wrapper.js | 3 + src/context.js | 22 +- 9 files changed, 2953 insertions(+), 15 deletions(-) create mode 100644 dist/vencord/birb.export.js create mode 100644 platform-specific/vencord/wrapper.js diff --git a/build.js b/build.js index 5ada142..385392a 100644 --- a/build.js +++ b/build.js @@ -16,10 +16,12 @@ const BROWSER_MANIFEST = "./platform-specific/extension/manifest.json"; const OBSIDIAN_MANIFEST = "./platform-specific/obsidian/manifest.json"; const USERSCRIPT_HEADER = "./platform-specific/userscript/header.txt"; const OBSIDIAN_WRAPPER = "./platform-specific/obsidian/wrapper.js"; +const VENCORD_WRAPPER = "./platform-specific/vencord/wrapper.js"; const USERSCRIPT_DIR = DIST_DIR + "/userscript"; const EXTENSION_DIR = DIST_DIR + "/extension"; const OBSIDIAN_DIR = DIST_DIR + "/obsidian"; +const VENCORD_DIR = DIST_DIR + "/vencord"; const STYLESHEET_PATH = SRC_DIR + "/stylesheet.css"; const APPLICATION_ENTRY = SRC_DIR + "/application.js"; @@ -32,6 +34,7 @@ const VERSION_KEY = "__VERSION__"; const STYLESHEET_KEY = "___STYLESHEET___"; const MONOCRAFT_SRC_KEY = "__MONOCRAFT_SRC__"; const CODE_KEY = "__CODE__"; +const CONTEXT_KEY = "__CONTEXT__"; const spriteSheets = [ { @@ -183,4 +186,19 @@ let obsidianManifest = readFileSync(OBSIDIAN_MANIFEST, 'utf8'); obsidianManifest = obsidianManifest.replace(/"version":\s*".*"/, `"version": "${version}"`); writeFileSync(OBSIDIAN_DIR + '/manifest.json', obsidianManifest); +// ============================================= +// Build Vencord plugin +// ============================================= + +mkdirSync(VENCORD_DIR, { recursive: true }); + +// Wrap birb.js with plugin boilerplate +let vencordPlugin = readFileSync(VENCORD_WRAPPER, 'utf8').replace(CODE_KEY, birbJs); + +// Set context to "local" +vencordPlugin = vencordPlugin.replace(CONTEXT_KEY, "local"); + +// Create exported birb function +writeFileSync(VENCORD_DIR + '/birb.export.js', vencordPlugin); + console.log(`Build complete: ${version}`); \ No newline at end of file diff --git a/dist/birb.js b/dist/birb.js index f3319da..152e06e 100644 --- a/dist/birb.js +++ b/dist/birb.js @@ -847,6 +847,7 @@ const SAVE_KEY = "birbSaveData"; const ROOT_PATH = ""; + const SET_CONTEXT = "__CONTEXT__"; /** * @typedef {import('./application.js').BirbSaveData} BirbSaveData @@ -1033,7 +1034,7 @@ */ isContextActive() { // @ts-expect-error - return typeof chrome !== "undefined"; + return typeof chrome !== "undefined" && typeof chrome.storage !== "undefined" && typeof chrome.storage.sync !== "undefined"; } /** @@ -1077,6 +1078,7 @@ } class ObsidianContext extends Context { + /** * @override * @returns {boolean} @@ -1164,15 +1166,29 @@ } } - const CONTEXTS = [ + const contextProcessingOrder = [ new UserScriptContext(), new ObsidianContext(), new BrowserExtensionContext(), new LocalContext() ]; + const CONTEXTS_BY_KEY = { + "local": LocalContext, + "userscript": UserScriptContext, + "browser-extension": BrowserExtensionContext, + "obsidian": ObsidianContext + }; + + /** + * Determines and returns the current context + * @returns {Context} + */ function getContext() { - for (const context of CONTEXTS) { + if (CONTEXTS_BY_KEY[SET_CONTEXT]) { + return new CONTEXTS_BY_KEY[SET_CONTEXT](); + } + for (const context of contextProcessingOrder) { if (context.isContextActive()) { return context; } diff --git a/dist/extension.zip b/dist/extension.zip index 0eca0e49192915ae03c75d18962a63118c8f9079..6545b41288fbb4564ad0e65e380f0c85356cffd1 100644 GIT binary patch delta 27564 zcmV(%K;plblL@Po39zIEfAu18TL1t6000000000000#g70Agu!VlHZP<-Pe=8%eST z`ZM!aR5N$3<;IA8^YkV_Y-Te81NQOpXeohgEm$S7*yI2GeP2XoR%WS$aZk@X_q;cK zZ?~x2B4f+Q$jHb-bJ(p9TD@*mcpiQI+r+bB5=Y6P->MIu{qt{sf19ZHy2&8gZ1rQQ zl|;X?*2I2!>-1$bJ@*2?Pj0|-D(Hl ze*HC?oSR=M&f=fBNj94K`RBh)L_bG=^y9&>-%X;gwO+3sSG(V$^XqtU9rv}*sKZWM zNfdXhwRYTKqnW==f4Et+l1&b+lbO)YQfU4O5dFb!s{Lxm8|t>&9>&=&lX#$gN~vIJ z!k?df&h)&|kE???s&=ED(}VpeZpWRtJBaRDgX;*BiISVR-fFhuMkFA_gSemQ{0&GX zLHFugRBu<49EqynL?*|*b?f{*?&#%#Hm;#4bLR40Of4*hs+NpkuO{4*X1*szvE|zm*J%)kY&7M(VVhQNbHc zM{+H_5-^tc7rVz~CfMA<=UTNIsN8DUI)*6>G#SGEVer3cCi8DIjc=BQ0hSB#%cy*d zIX{nn{Vlrff3+IlqN)f=7O}nVM!Qx2R4}8yFx7N^7BN7F^hVso-MC+vth3SNi>MIG z2--LP2=F=DY|tCluj6hbZTS_*5$r&CAko!sJ?=H5Q`VCqMJAioc7lPVu8|!gZGw}B zS|D-_sD39NKRw*_kU%BAaM(7Va& z<1(^+)Ov$Kufsts4jsJ|jVSs?c=L3ShsA$)hwZjwxabu6!lHf9gEAWsFO7cnDvHT_ zN!05Ge_ZJ_=Q}VnD!g|p6wN!m+gMbPmbsqqz%TN>e%)#}s9`w*NE38j01{qC-C?H| z_g_RrtNqK9fOqw3yIrf+KfRAO%&(}~>qjz#oe2gYC}H;=623)%{>mz&_ksm}Z~$X2 z!Y2J{t1DeO4ciiQ_^Sfd+9s#NHN*1o3+i5=e}|f+Ka5{Q7HoXL`T>Fd8hv9W2ZfTA zV(^ohh9R`76cU-Fl@+j+9Q3f3ZuaiF=uaezg10tF8ok-^o`8Sd}ZNXP)i&|$N4CXZR?}jZX zXZX)}m_<=Q5%vd913Ig?qi?NhcRXq-RF5eU0c%ZeD zn(t3R$&tI_d42xho`Z-Hm12ev%6g*ws0N%mivIoY)abJ0HTpoLMjLjj_tZ6PSWN2u zUb}tXYQPT7vZjinl!dJ+c3$h9Sk}t?=2|H2#TFaoA0v{6ZIZ!5JJ$92*XUpWfAjUA zI=C(Z;==5UNPoAw1 z?j`a15Oz}ceNZK}Szs~r?#fBMe|i%KhKqd1eyEqg08DpMS~L0-Kd`^-qQ8OavgQ*e ztnlgZu)x!Sz$9FqR6USrUPYrbtO4(pyolJVXf@)-N?^_c3Fz{c$8aDRm(fqIlUlFw zPz0k4VBRDFJXVRNMu%*DWX<>Igzn-ZM%L}N;%U-0CgjF;&Zn!}E;`ECe|RsP*NzV3Bn{HNN$zj}>- zueV`7uESV)`1fvEJNWm}P28_W2d!>vrd1ro$)M1!-nOvP_4-A^Q@VmNXU%KQPuv>q zo^=LL>fll+@{2KmplFN5e`2+NB^%ddq_J%C4dg1z#=Bt9XuH&U<2xmRXhS)u)C|*J zY$t(fHU={5CfSA=S&iD&2auuJ?{)A;a?@|Y<&a#*EbI5HA`sCZFcazHP=~_1_wU|+ zGgT{6le>$c=}qfCZs&BMM!#*m+ik@6Vw9W4Eq(|@+BUe8)rRBfe=qDi<7)ldjH*o> zw|QV@MCe?0 zKL(}Ptq`N)q^Ubd&F)p}{2INJ%_?^`1)B8`G#K>w&o2rGeojppMv*4Pn_+V8L;vpK zeKElfKQ#w{_|G59o2MOn9xC2O^A>!a6;o;s_C*0Nr1q2;KsAPsMG4G8?}xq z@C?zzg|2R^{f7wqGt;GkBEGM;huppEHgnjQ%@?fEew-vxf34b&lGc|vD%|ww&T2uG zJw#XHO6sEpTxm_hW9IZchdVN$eg%HO1&47R{S%^ET(3e=5Y}jZIjU5f)qaaw4BoeX z3p+#j>l)ErulsBOw*mOMuRX}PGsyt1msFAsNb4Y2T_$><3omG@2kUu4I z=$|1|X4+4pe^9l3SAF0dWWZ@WqG58kG}<4=tT1L_7RM3frUlP)uMe0ZqtJtNl_-vDRq#Qr)o%o9KmE+P$`4Hal$w9x`O>~Pc zrAKF{`@4r_l=Oy!cB=~`A8po;PBzOY_*m=p!Ef?y>*$CbGb z)bh%XVnvG+$ds8^xdgPIe5f32c@qwGoB~WIH>QT1HDEoA{un&m#L-uH1A)U|bOFJ- zAP)53zWFpgKG{7jf2iz~%cmbUj`ojEJ{**ef1$O>WC6im8u}MKni83le{ahFCS3)c zo|etZ|I#u;a#J`rTb)}=3rSTinOmD(n4L>YKwmAWHdo`7rL=&Ip(V=;Yqe(6D*+Q| z$=X`Hv^1NRkR7z7v9`3bypWcVF|?$)TAN$+N`x1*D6X$AFRY}EQeM&WrIpocyquPk ze?PQjVSYJoc=L6fqs8^r`FK7~8wxX+K4<5ubJa|d;UlflY%bT9YuOsYNoMes1^km% z*z%bPRfKos7$SlvtZ^y~1p113CC5;K)$}dmu@%aBK$R(J#Mo1}ZfI+U6~^&nz1N0H z3hP>{2Ro!e^(uKUrO=LQaP)_DXfas3e;qg}z45T0k-l&~gGS)vckhk4F}Q9eMb^hh z{s*Q3T;u%cHAk8EmsC;R*HEw0hIDSkV`Ac+szhHT-b3EAdM>SK*q@dDZbXCBNh_%A zNVyTKOgThkd~1lw_(EYPDK?B2_Ux-BskNj$msd;JN-7z)3o8YzCDq7!V7{;#f8`LR zO0Xieby!Ym%qa(>0%1?oa)!h@2Ibz&TC!)!C{8fO4qv5Hz9oEld9^oB<_ zaqkBEMr_Hxk{T8-RL4Saqo;+St`Hho3XQ4`3Pr(6sZqT_X{uwTic!Hq6)ICHH2Qa> zP}XRvVe~L-e0$D+V~Ool%U!sLvpB4m#d2Wcr0*}yS&r%li*s$oyo@8U!pdgclzo~VhA zyH%Y3$I;DoZ_vByS8uT9Cw~(4zEZJtNl_oy;;#!EF3<(-5^=3U&#gU*kS6iwth3%c4e<6rnYpgY^Yl_$x z(Ys0Fb@IK)K|I7k2?9(`;GXJcF#pv~rm~8fOF5PGY*i6rdy*skx^&XOCb#--VH6vJga^XRua?y_) z8`VCX2-S9$WGCQqTQ)+lM5V|u2J+Cnq7mrRW8Kna)c$MC@g=Lub zfs{XOaafpJSX$71np;*Jk>b^b>Rg8PQzGS+W{A|NFRWzYe{w`>%rDH(Jt{l3In=1j zv~Ua_$PP?OSw^>e?FN{)ew`(TTW4JjLlPR5(C=7V;X;D1s8z)Q`~&u)y*ndD)HYEw zjTA_;6q%o2nm5alWho38KHOV z;x$)w2)zV`AM|of;sd|XkRr8*OG98A>wh&wHM}gKN2^Oqz_u6rfCe7|GwCmjtLpYt zIfN5ce=5K;{fLdcTwkuPs4{Ugr|U{>)TQ}&c|6h^%c~0+jAoI(9M3f}LgvI^VR0Tr z4VgSd`uytr?6L~5TV_hP)kgKU%J`;WqrqjymW?2lF2|~11C}m07&cf`O0qbnL5xVd zUZgw?%~b&@?hsOi{m2&8X^iMAcu~Ra=%fY5e>hGeJ|Wr_8gJO)IevC8l4>z%{H#QK zoWHUJhf<&|91-fZ*|qr$=cSxqjptV8GZsUJ2&)TowQLnfgq3&!UaoNHkO+%wv&~tx zm6ZrM0`7ykbM3Q=mW?sb0^Y_lZdZ3T#_S%?ZDh(r#I!uz42GgZs8~ku>rlA3#(%fCkM}U9A!W4zg`qJ`u__$H$KVb(Bh4VH3Gg_E#IIyL`KVgkrb)G_^sJ82QF0S2L z?%TrWh!TvsX~04*9)GcO%@VGA>g<+V7IspjCqAi$W$_l>^6-`;wohCWFvE(`?tsgD zCy9n#gjHe_NUQ8Y6h8iQ4Jqi3Qprg{f4sWZ%yOF};aYQbcB~ZWo-IUSO2W9dQeCKq zr5P#6tKvwwDE|xFhf)Bm4F7~R@<`|nqa=ht^l+?DJ;%vaR%gL1ma>S2+V|6Ti@FG7 zlZaKuEZ|gLs<*h_s(Pvq`z?e3isED3GR|^Zb+eNnFG`QPP2xsYrRTT%Dt_vh=8y%5p@!{Ve-ZrXn;yAL{9OlGe>fF@z6u^jJ?w%L1|`T5xz0N4 z`PL%i+xEby!~=8QV?2f$UyFIr8_snbHB6KXxmAwor8Bxff?g-sNbV!Z=OX^pa78#R z;a9bVDOv@2u2WQyqeav+U`{Y30D)_IED3OQxFQtU5C{H@X+r=t;~6B>f7r#nrQeH+ zKm^BiuLjr8UqrJCkp%i^Z-|#Cq|r~jP?MoJy}>x@7c`VL>UXQvhe0AuqYmsp>~Bpl z_%qY^8QI5#|I%N}x%1e-aZWm}fv!`%DR3BsOXEK#oIwK=!V*SAATk<|dw?(|80o*J$U;1z*Mo&qrZX|&Cgg>!f8^BmVpzxwuGKvT zLa@udMT0`gfEGnIiad0LZ1nCSno1#l|L*?1ot^82-^kRX#EO3RCxro7^`-3n4VR8~ zZ!u*!T23N38zs2M=DyD2NgM(58S*h^XEzcjKSsCvy4$m;%UR6BC8i8}8+P$qNCldJrZN9;KW%q;mP255oAhgRr{alBW zhNq^Oltf9za%P#Hj@sC-1{^o$K9t|w7UQ%9Bg$MWi2ZR0z}1tx0tUr#r|p2(GlI$t z{X%?JY!xo$78eG^CNXt!k^_cCWFor5glMgZI&5-L!(bB_e*)1XXoSPs0Yi@QfAUR+ zDoU_b@I@nKR1ZrBIQAGAEoONorJe!scj{=~ZPIp5hCIo;5g~W7lLn5NkTcP+W*qRm zGPLW+6GrW5oE+-;ZXND60P9dI0ix*@&mu2(Ld-Cx$`*fQ`-N_~$N$e6Py~}yuNB5~ zqjNWPJ#Bo)e=*<7Q6YX9c_gOUMv#Axv-J@QKjeKJsg?O)R>^M=M@l$lQf--1w}n7R zkZpwX#mb=(BAk&iy_8v5+8FlbUPY#Yi;!*idN+=T1=r^2Ub_W1+YYewqb@ndg&pgX zrZrKZKLk@E&8{gaXS+#@O?Ui{_YwYwE%G}25{TCPe-#&Y!$L=Nl=0G8SbEy(#5gzV z^M;fy6%7XtCoTmSn+6ud27aYP_`1o#>8u`jn`&b3RIvoqtVt89&21BkiCqP@E4Y#* zCMb?bFa^eKHRuP9+tnNX8~{c^xeLs&x`ibYpz2peh#_u1VxwZ9hvAC(nGO_mTs^j3 zoI+pdf4C-WEC9(-8tB+iSW{#dIJ`lLF3^ZMwYUO4<>ptqoYJbma!RWrq_$HDo^>(6 zQC-^Clv$!2vfs6gP%>g!uz*heCeFw(v}y_2bv2a5(5IpDp#g&u&&naee6M9Zq5Ma(DR zr3{97X~-_BVI`*xMoDa=o;IFtB3KN&!SWGF22~|PM2a|yP|J1-Us6%!NdZrc5(N5n ze;XMcY)8wi1sb8wuwqDX)tUvenDOL6SG5qZ9Mi_cxr?*38hfPe_0OJi|KSEsjJqB)nlkD{pT+>(jc4%mOckC z$NERQP#4s{f{4x007kyCMil{6}D(4r5dg-4F~ZtZoe_q z6pO5PJ@&Sh$n^zCjJad&P1qqq_iMZ&H(|KL6`?ctJ1Gh-}fO4XU4*puw)k5&Lp?3)) zY`X<%9&pbkNQ<{B@636|e`6iYjqgA%794e-0LA=wIyt@TeSazo-|OVMA3p(>#qr%R zhHd>xXkE(bu83bq0%io(%LhnZ5k5;>>i@xbMDe0F2I4aGF4-=_j>Ngs}8 zyAOtQ0{!0^4&8^trK0~|8ju47LvA71PQ~%RG@_egR$6HPsIyEZrb)Xvl^CzsE*0;x zCJ4?Ie?)}dBP4MS*GlYk{njwvg0>=*zND%Uj()b1CnX#~t1QmXO3ci} z1b%6m5mkRwwGjW@z(#YLQAAysxmu8NikxU_Sqkrh1$Wi{a9@_qmpruDOEW3bb1~E( zU^UGFM??%fKemY6Ij)e5I zujX&Xt=oKrE{G7@e>jYk(21-7y78oY&p?-Y2sCdCfAG)!82Lf{GOlUfsX}&&J@@2{+=)kIfxk4 zO{I*V^SuY*Ot;Mpbg_tfwU0Q+H1In@WNxBvaV7pOIBk|sPywSyINI?jitWJ48J-p+ z=G@54f9JO$E@PVl$YBh`HsA7w)o#B#tQ^qNk-Jf;gw9+Cf^iY#j4K=X@q-(k`Ljw$ zc59ta27?Ww9RDO-y6=7jFw^~Gkfqo~MmF1(Y7ZXBTkWCnKAKrm5eZ>I8cjr*jFQ9( z+eLI^oKuGX%tdMIqtOTgBP(<2`_7uo;P(g$oNsiLU)k1nV{hd!Fr>V?w@(7=-c+FoaV> zPRI^2!V!-Ra+x=X@I*L>Ww(^OY1Ke8e@VJlT)u|;ML2EbEZnt_Vk3i__7t$-ihag( zA3=Thn*?e)sst}zgniml@hBV-un(Mtt7|Tg`_zPfP&1 zt?FdTey}3sbC&q*P%uS4 zn$0I**8CF0`iM{!+Gr)Xo=5FwTl^kPPD1S>6%A6nj4~2MNUDy5+li2c+(Fz*-WY%l zq`tIzV44FaT?Z?rH{}nn%9nspe<)Q!EG!Nus{~DJoZy20hhwCnVU_fAMY6{no|Z38 zS?~d=WaN(zA5h23WIwRSSFO$sk`EM~6=z6b<|M)iPVr;%?0FHpjhY?!8`BxMzwBw2 zI}HOL;+7DtJF%AaxM@T8PtjKl?1eR@b;Xh$!CaaU8Vd~93I_Vg0-^vSe;1aATr)@s zfqYTVD)KD zsWuIki6>>FDtux)G@ZeK)zD^3Y9rjM5<->Xtjx5&$>!beI&Z4T0K$W0C|G7@N9?;K z)rFl-{bl9Akr;p+qlq0CT6756|<-sny9oM!=B5BAGdD@mYCiIoRh`=Fwl)o zVuMjv$a=uI8o?~>Pp3YjE1*}eYa8_Vu#8|89>QU~=$lqTYM0y;3T$tLc`WUJ?! zvSCI!>odr997CA8O3Xvh^4#zfQ!OI}G8WI)pfLISB;9gEe%N_hI~^_ChK;z17#)K_ zKBk@N$V}6+u}i$I#-16QfE_cNC~qS`OC~Yqs2>zpqi~L^f1W}O3&sLEf~(-Yn7g~X zBC=)-hBYLD?se?NCYhq?PDd-=N?02+*&#OpLJ1IVl2;*}PVv^xe-r_XN|DeMC*wB8 zLR1f~!&O8eNy7xWbNDEYl{llR(guP~UAg!1Do)$1tjMN_3-L&JVE@i6=ddP`pi(Tr zz`bI*9uTSrf32069>TTS!0|YK)Ax8d;R%EZK3<Dp#TV5@yIh|48<0kqa7zUEHxrWEv?>VF67;^m=p&Th&0b9!A9W!3QwI8$KAN3ASx+XKoZ!ZKlyv z7L6zge+2ly#MddtU@iFE?S+N;YzQ0MT-uaKP0I|>^1QW)sgJ>kSJ6X<6At7{`z8Wn zdNHzPTYVo-|3!%zkD`3yV}9UpA-6X_JC(myYW@Ul4cLf-I0SE0lGd?hgD{o3+9cS6 z{$G-(Ixef?U`B4Tx}|$kLPb^Jv;vVO1;b2FAbCFjs#69Bd%Q0}NdC%7gWVspNkD-C@M;GhdJ#0|Xbr@+jFbRaw4(m*R zE=jx13T!OezgB1ch4he%Lgwir|N7n$HrzyV0%i_Tx6 zfBrKaaRlDD6lCXSyHD+4pF&SEgT({xy915Fv}De90(CsrZC00C#m4Q!&GtLd zypzSdUjGx4%Gcv*Tv|4HMJFbU_0BXlGDxg9(8MPAw1Y*nf3P`S$H74jX85P+e;Sgs zn&dAir|V;wOe2w-YogD%Oyf}%e&vWk@JKR5k6cA^z~)0WaBJHq!Q>$o$KW|LKZYm4 zZj#3kN^yYe!3=e$Ai22{2HoFoq12@Jpz@W6C3rqe-SVXkeN19LAYKmJV3(Fg%N zmKE&}Ge1&I92N$6JF>!+K?Rp}+dkXTWBmV^qs)v;jLNpi0-#fWtk)-bQVeAcB5Bn* zM23AU9I)TKCne3hnl{!bA17al!bf?sDz4*w2;c~3rbUFtxXmc4hCFQGe_H=uylusI zzuQNiV3+ItYlA|s+3&^AL*%NiKqE*dc#(L0$_;Nu$QCQTpDl8y(u05VUyZ2;`f zkNC)h(I+%5)1AhK3B;4QRu*Me+kz_?vx!J6?0cTA0&ajoi`Wx2b5d6MuzqQd-iYo= zaT6e0zu%HUlaoX2Ry4gne@aJyl`6*Yz;4}aYEW|wh*H02tO!)gOwl|GU=Q3j`6zP- z4n)msWf_Wx2gaLdUDG^~^MalXDj|llR5=3@8p&Z92X^Y5&0yr6cerSi`5S&9Q68)9 zEGE5G4%*`|2mh9%Jvc1P&kQpd^&_^c)5a@I2{inMH(mC6AsQ{)(iZWtM3&7^^I#Lke+-*EHKUVWL@#yC2?|0; z^VsGc0eufn&ta)|2GC5g1?C+99als^wT#DhLPV1?q}4!_Eye^HCvjqGhA@AjNBqLA zIrA#ytPx{bZN2BtO}vP9!`?(_rB>RZ`IQ!F@A!B6$&ADQ@Z&e#msyGY!yqw5G7iig z!VQts`|T~qe?0uVvc%U?*o5vK^edD0~ulDYXIL;&ObJRdSpOiMMx{37B;}VrJuPzb{fdMl?MT#9g`Ie2ikRDXV z+c4-{mJ0mzAVhe-RP=k+NE`Sspwva;3X{{*aY2BNp> zG2Z4-i*fQ?o`S#uEbsa=^gx0U7)K9_=z(t;$cS9pX(Caye+oO))de?@on{cz7dS8& zAUZ;ln8{jv`@HXyPN^le##J%cMjc?=ucU}jf0c(wQV%$Mvk|4E4U9qzTkxZ8LIebp z@%m6G!bj&=@K7;20~j6?G^*e>h5+YT=Ebu>?A9qaj59Vly*4BU< ze+69l34sm<2D}e%l&{&^hx9VRl|pQ@*M+U(38%evrRx-+mmRp3EV>8mZ#=9!s3IK{ zjAJfKr}z%At47ugI1EUqdx$Ag;9y~|)Ph_l%sV!4uppqCFW(|Q)AXJ52CxbtNSji0 z!lRsFP?FX5Ap#=9O28^W?28B&-n1Lv z)jty*bxmLZ%vV0xiD{ca0YsCR5%e?QTE{fNg-NXez3Yvnsty6Ir((o}5(Jh;T=@aK z2SseX3Z`T44Na7Kf>hK8yEXb8_EM@@&{6sbwi|LE0tys_V~Wg-8B}f^w({-3Wv6rlz^^bcU| z+jDvHoMZv=jXm#;2bKw}_7Uq$-T-#ELMWeS)GDR)A@4jBjN{<-L)SX6*wkps+FiWJ zmNrN{qXzq!e2a_TFo~k(E8Pf>e*s5HaDfb~3lOHRbhV|2vMhN^XW%p`A@yrc*r9NE z&_fX~%Eh*3`*JXgB`ZtOMp-GzZdbvMNsei$_~@WC8vS z0SgzZD&jKmxxj4tf-&(&>Oj@5Qk;Wd=o?CbD%iav>|NiyBu{3TYdkZ?Cg}86sQD*v zo4&&rrq814L(#jUjKx-}f4A{A7OoQ^hfg$v3tH3G?`@R4E3p<<*c(@lv@>;%LRaGY zme;#36uI4}MYjJG!@T&0&Y?#Wq|$W99A+LUd=bs*TIsbh(;L%>r8%vTXPb z+Sx(*@a)6xCc4vYe-i?&hupS!rR9s=(`*&En_JvbS@+Tbn(ZPmP|z^exkT*_pvJ7W zJQ>J7B5NZ&qj$Ohah&(kMb=-$zVX_X^zi~R%jJr-F*9x{jKwIfAG65tKU7cZB+{0s ztv}L&psVx`R)As?Op1qX14$VrH(9mquz?XIuGM|`idKIie|l8kI3$5+3W8wB#hSbF zA4H<-ZahBaP_!@(_i8>{cbkF+47+|uaJU)QhF1<*7JGS^{c8~U=a?vO(n^U|A~V2Q zkjH_7g@9EmF-=mO%DTX3H)qZZ_<;)2M~@BP>X3AV>Ofy3yD1S4%sL`%cknzzCywl_Nl-cy8^qxE4r1*!6yIeu;zDOWf*U$9pxO#XcmhKpI7teviCG&;i(ANph*&w%PKlJbS?jUAA4)Zz3< z^Z(Fee=8UeX3KG=N+DdoQch?f^K}0`29BxXTeQlKmNICpT%2;LvY~9BM5~Ax&^B_6 zVmO;3`qPMzZrBASUDKY^%_WSE4ppV6C!_|%!0$I!Pgt&#|K`{%pTT$Iw zf9R|zKZUtW)NU1V0T6M_<51N~2=@^VTp*-hF_Kvpo`fU`pfoWqKC%9wApxtKU2`29 zzXtpTnm)nvMfGbtRN|}qeE!mTD&YiEKgbe4Nh9c+pJXiGeDExj{PjUcHTr>~3Ji^e z5y@Gh5p+{k)vKsO^Tl$Dz9GeXBQN!$e;U*^d}Bx?f>lPFWt<-7Nhi|}-ul4vQ;#+eT<9hSHaK@+;!`AWgk8rq(eo2ezv1Uu@($f8KJ# z83p~l){1i@XXZr^-g%RMuj)B(Ugp-;W_hc0wtxBoVOFQe>s$@lGA8=3qe?w)Xv|mPRzRRKgp6vz&(7Skoo`JH+do){ILkf^GQ)#lt{ zt+85LXspG{vx|#M%QMJXGDzDMdxIu|D|upr(B@}|u;`MDp3z_sIPc=t)ioaoW(~$} z%2#o2rXL}@FXgWN*S}4ef1@4~V8O$)m(lzU(+Qc9ZhSQzl-8^5x_?UK6+VX>X?l*p zn8uCX5LVp}wV(HzbL^*dk%5t5_}#e3la~?Pc-4h^ErrmJEMc^nJN$BuJb1Vrzv^>s z(NGlWFRnMLX@8y8y)6?D-!wiHaP2hdU)2}}m(zc=H2aUBC_9W;f8z$Dav=9I`k${M zgEK(R)Uc;^tmMlbs(|o+r^2A-KzhNecv>Cl&=M~-R~^m)8tBUA`GHnJc6WZ!bXG6r zksvt?BoE?R61X8aGqeYWKWT;;s2wzR4W@hj7DE1CM%o>Uz913=zEd#W+~UgO>caBE zY814EqE3297<|)1e>`Q}mr3Yu04ZDK0Rqk@1=xM{d%?uDO#=_L=!04Rr*SYF;J`tz zW)UF|l+vWrE7)FlprGVFM$p9s-rHj>cT5pZHG04UBS^vV2uBTWusJD3oE4f zcMLNkD<1QYa5y-j*1MlhuB%9%_Y&)xU%AH+@oDO&=h3V$e?z|TN-((BYD6bI-ZCg1 zY3ubnGG@>+P==4S^2$av+z(Fk$2$mM&eyWfA4PP~LEiY@{Bkfhn@dNf@DH`j8yY75 zK$1FpJ>}JV42Zlw#^LEn{R9Xb-;)P&z)B9Dpr8C%J=%vAwAL3<`eqQw9tuo;G${Rc z%)j}cX_sg3eRrQx0X?FG>7|5z(<RC4Xh-aUX@WV!* zf~VvDW6``E@b6POJ5PY(>OZZhOv!Hah ztzn+fe|$Dyv&+Le=EG(|(MUtD)3BLW@K~cj1$k{oH2P@S zI9)#OyI3DIwT&4v+N0J8q{8fOWYaRYOqyqzQ=IzbO&&8%3nO_fBnl1&{hbO9uXasq zk~wacy1bf@15U=UA<&|pkq!)7Mz*}`S8-7xe+!EQgc6q2ShB_{x)8pd6TUS}$YgR) z8$K8Ctlew{2KRKSl9mDyF+V-$;D_clB zcjQ5FmGMyy5#BtfmLn4~nI*`<{f|cbhjvKUmg1D-njIt2oR+NMgvzXm3$wZa8O~POoSswZG#`y{PMcN9 z7lP@zpt(iU^t`ThAy5-lCM+CG%LZ6ff1U%gGfZ0bX=cZT3WU#24n6^8%F4momwr}f z2TA22G+fUD#tWEvq?`XAi`fq`o3nkC{3#i6q9TTp$6+FAV)jk@Te>jYa!F`~ptK5>H`YTj!nFVnyv-*nI z8!Jb$Fr=w$MdrKp9gocx7iIS1f4QA*w&HeU`f7;Oo8H71=HassSje&O6|!{t8r`9n z1%OM+aZC-}KY%|N+1OqpQ0^wi`2yaJtPTe~DtD>HKc;or1M8|Q;Hrv^ZbsHcQ>+Hc z!%jQ=`k!cu@%l2?E(!6$U>Og6%c{UE-UTw@x==O`CzZHCv3= z_Y$5$_y&gvU!FYb&Jiwu+*rV2N@0EpZ;8&&;@!@<#l?&P0{7x~y>cL5ss_OGSGB6? za~@B(&aI&1+2Rs99-SVZf0gDDGdajol(K;yk-G=piI>q;zxtqOBh*RHae92ysumXT z^RfQSTRrI3_Cw6{k+H3?Hm<4eR}~l~%lDAy(b4Cm02^K%ccSHkK8CQwNT+&ly=uPc zF%LtPR5E%n5PkD4v=Mym8f;cyMxhq@u3?fh_~dj3Wq;NOgb2?K>>9@Z_0ex2lc zO#v!yp0j3%Q>*-qN(~X9I9OQ`P()QEXPDPviSJNe0nd(>x=|KJit}+DcJafL-i!<<}_LT-rM+Z=J<^Uk2UNt^QWIe0e=s+uJYgwHkLHhx3;=jrG#)m(s)TdG%uN z;^w3>d$7K7S6)AV-P^poy1uTKuD172D-Xk)gUk7|&)aLqm*=lfYAX+W50`u8-D>^5 z`|0i6T&=UzU7K3DTzK1Ai|Qx=n?LN;UcIf?yEo-8o#s|tS~@%&9^P)x&Yo>_ z7fu#Gzgg|BFRiXE&Udapy*=Ciym|HJa(Cfw@AKjD;Ie<)+?i|5FT|_e*YkjH_13H2QUC36>1}(icYL?oIo&ER?RtVC=<>Tw>&dlqF^65_VZ1(eJvRj|J@4nh??Vg^O z%k8CqTGZO_1*qhVyxBfo9)4La?{1u5zAEo;msa{~M=P^? z{Zi%C`EKd%?ak}U+Tz3J;?~E~UT5XJ(e2;Ql#_+!dVTrL#q!6^wKwZ`o4uX6kF|Jz zI5)engce^a+eekl=KkC6+VbnG{fpI=!;kaz!-u)k>hbp8aO-Z^uO}PJw=0#K`ugSe z&YS+s{p!ZT_26czF?X4)zB!&++?}h`jt^Q%b>nW>jaP3kr;g9|O2?ZItDV`>-QmJ! zUibjcB#}^?)Cdq+n?6Xwt&Zf!R+bjn@<<7OWpJK?M!Qad1iI$)!F{=_-47Y ze^q{a^tSd;*?U;~xYFM{*gjc5Z+%JjKOcAQR*&oP!snaD>GAnhWqk!yTTRzB3GS}J zrC4!ycP&z&XmNL!rnpnA1eapPDHgO8cXx;4TA;Y~r|tJYudS@RGPC!dGiPSbWZfip zX7cO(>T5#;jJ*~!wlmmPdT$@MA(tihGh?-fg*^pgQv!^J4k?0JLql7Q!kTHeDPfSD zD_$pCOjN&Xa9eGD@W=pa=NNz31^&sSkHMCtv%Q>6ZC>cYk)$;U6o>i1(NuxAX6B zMs~I@9fEkb)%5L_V$Wx*{a2UTjNa%kJvRDY9Gp6W!4<}B{JJe-EdtqMB~Vp1sBQS! z$hqZ@%fkqpK6=XT?2DC({JxTfQC# zPw(p2z}|k7hvmoDgx{ae`pgds4D{MpzA_l6lq$)ELnw5N*$mizj9?mHQxN!_X}Qk% zED^mEZBG|-Y$ztVtevx}$LC%Wsd}mQ@u19@Wa#V(GW9`Z&%yenEvdH9_1Dzu%*;$x zyK1us4er>D)!Ya4xQ*ElY%WkSW@FAnx&5s-v)~UuSE40aUW>Ng>-q*v8nji`t)67J zX1@^v2Y9;CX)AgnZQHxd$LSY!LTBD|~@L{fgl zGR(cky5ew@W#1}TLb}$LC35TPyh?losf0Qii)DU%W0w`ZnRf8n1Jv#R*zCcxY-32V zJPU?mT#c?~8a5~)-Z+C>AFGMHc1MMrjKE(Po0f06{qMhMKQ$U;Ve6>9?)kssqNku`X0_Wn-Ph<^k54oEIho0 zmlpS2W&J^ZlD)!`V&(aRaP9<=f=`fegzf&mY-K1-Ex1=3dU0^_^kfV5`f`8Tq$$|k zvhuiWPd1uzg$erUCImT@vm+#2LM)WMKHlH%8oh=5{LqP3IJl6JpQNJ%g0!@3!Gj}{ zd7$OagbO{vj>jq2d}8)F$BrqFb`7hQR>=vipca)Cw0;=d!udTz>Erz#qk=9eTBEMc zZ}XNRjw^#@inDbK0)n!Q4o$-ZNGZAsK3=~j@y$(bc3-DpsYLTf(CK8X*q^*sRSE~i z6dH<04ucqVoZn@KA#O^V@JXgJXM>j|lSx)gv$$I z`f?zvhzQX&k(K#@V*uo$4Z~7yF{Ao0vjZLoPqRnUEGt_sO%E;1Oyz9YcPy*>ux{I> zZ*#;m@D@Km9zIply>VT^75s7u4r!3*k;#A7KtIY;|m(NE-zxgBK0XTV9LQ)!mcm6zrgI?NJ|>Tj9j&+2q7_)G3@Sgnkd-?LAhu*aOA$U)YQ_`O9#9SgO-~ zNE%r=DCMz-!=5{LnpVW_u2Q|`Bq($G3GQco5~k4_6r)mFzTmz(tec#nx~>oEjhGW` zq9pm@C5&!90~+!4j5hamu++q4%NZES$`fsKtJUwj>J108H~E&(A>yKIPD4%3-nc zkuV($Pude0aokR_$+D&U^B(8%zOh(UUd51Xwer<-*6s&=es9mmd&qE)K)~%0iSqXB z0`C6Z$wy2zbZxMD7UVK0C3*en%mI% zeN?;7ZEctpk}v*9c5tU@&`^QP+mj}5W~7M_b{B@5ICt`IL|6A%HEe6Qfqcf-f0s+I z<%zf+1IbP*>-nN~WS-{qu5p1Mo6!!;y&r%1%j7$Oe<_9spj^GInQZp0m5zuWL@rw|&Xsym=nY2D`?PfIewOhUl2sLoj&gd@y9$AM;K zP6x&uOgs4lIHd}5O>c(ViVZ83XwUpq;`sFk*H*CLKC|MBVLAS-+}DK>X+;-uM!!DY zaKgJ*9W~{`?~TPldmiR*Hx*uO7{cgIJY2pj5EHW9cfJ;X*{66yt7y0tOiQ=2g$G>X zP#dCOZD;?XH!eQIMziVRtks6PKzB4r@ygvq6io2s5krSq^|h<0n8o=0I1SW7INtF^ zP-&9Ba93=DPgpc6D7{?b>p*c`PgvVXj?pC>oPw3ZLBfw6Y_nz=OQG<9Z3`4BMLZSN z^@_%^XtTt~LX$$b)eS~>&&fqP;$)|X1)XXhbiGA|_}4}3O`*)L#^2Lw9W~#d{G(Qnzasz@n6VuNEiB||3xBb zqV-B&?lXh*4y zJ6~UIS$)}=Y}*q;CVo+os`R;X>`2?~f|U?#D)Ykr4fc81h$`@EGLTVSx9rSGTT5cd zz$Sh9MCLGJf=H+lxAzCe%PC*~(&Dx<=G=H!&>3q5NV{olExvz(_%NQ>4Ha%Eacz2h zRCzJ;F>l4%F?sm;x0A-WOjK84a`RQ^5@f9%wYQ7pz|78e)-;3#h|E0oeOpUodmt_t z>vKMmS6Mt1$L^h0G!?!|d4laroh0i?ryGZJ<5}IJ*aRV^z9+T!6E`~XiKLjpM7Q39 z%TM+~;YkBO8)L0}HY0W)mI;Q(Mn{COrYG4;TzzA0TTZKBP&-l7zgvd&X3O*RZwm8> z3AJlwN?BiM464Wzh13a$4ruZdiz+aIP1CO;T0;nTQnBG-&rLRqk?LY{!_u08@8Csy z!rj*Eq>io<{5)dp(a9H$QpV6)_>%`?P3pb9H&-2p7=b$rDvmvNH&aPVC~HNYO4lbl z?`|!)7i{aO*tPmk6V`tmuEst!)q^7sk8URI#Z1D^UmXKHkF}bWgC)DF{DCN7RJ=^s zcw7eOq~uA0P4g_ocpxGZgRl(H)Nb5L3TgjhgT?#%PvTYDxv)l`5UrH-iP7OI39I<& z1`5{Gw;WUC&Qw$|{pLauC|=&qXoQ9Q^}CE0kARGpr-CdHRVd<$OZ4;Fz&G@Js$zwY zg&{v(oo>`nkc<30BAmP8$;TJKUOugz9;-G4gM&ZOoheN>rpwrG=l}$@3G|VJalxl--$M9|& z%T7uhFyWJ#Uqt1qoI7QVNml3CJ4L~iiXspancs9ZY$paGt8%9%SFtyJn8@t{?cuwI zk?ql_G1R8ni=mDk-QA-#6#MB;7THgz;24eMt%mby5c-UKHa#4QQ08_h0LtpSI=p5% zseM8F25P!Q-2qLbWRc_mlU&@YPMVu0f2#CsK>83ItC4ZW`w5?Pt??=ihx^(`Ms|#@ zlydO$yY`o%Y_|c*g_t4{Ph9s_v!4{qi+Tw^EcSR&r>=ZwMA)y9@aNuWh~#&mmd{%c zptbJMix8*>?XdM1-S$KLzxee!$ocRPb0Mxpr{7ixzF}gF6Rb*&M!GaDV0)fM z${323QA4h{7Iz!Tk$P;bFvBtIwC2tmW-@p^$uNXkQ*ORjw)T>O>M?AWb>~}iHv>Ew zufx;K9&}nNS~38c;20i_(=7L5+%+wpb|F=ZYWb$ zy#JjF?Ds7i@L-=)7&u$8Q=IW;OmH!6E1q`#QFjrJ%)&N8{WHO)8ZmKQw;KObL!S1& z38uCbeXr@PIKsQrBp>ymeRPNG@S%ANk51@bvO-{eAzhJ{<dA)^cF`ks+44^B4t1v$Pj_GR(!btX83Dn#!89`kT{wLR%)tGw4`QGF$j*X7q^_*2Yi-Z@6an5O~jA;(ASt_TEfWH zBV(3?(zS<5yR3U&@3!Rgy!Apl*SO`S8=Kq<_@%sB=JkyF$T!EMx^wiwbcQ(cb<DjK*T#Y(drC%bH(W0nl*;Ro8=Y|LI?KZYUixvCMZGAmukuul zGW#ZfOheWr*vo#4{smYWTo=C8JQG6AgFZ9Jfk6Pi5N}!bD4@Cs1-dGJmM?$}ZhVL` zW54su@f%psv`*aP>sExLY;@Tk;SdgsA_A0~s#l}o#s6BE07Qs<<$}ZTesrlp^XmhCf5e-W*<9_&RCfP#ScUY#6)RM4CgYllX8u>K*>#+%iZMUZU4@(Ot9)L&q zHNtnWs1cA!J=t5aqonMHX4JpuKTJX@cfvdCZV?lV=Ot83-*^ue0rrKxSdUqHo>wglVFOH z$87v1Y-3cWBT|h+;>le%$XUNIOTkwcqi6>Z?yqd4jt*jLf4I=Bd-Hx?j&e9&{E+u< zB>G%J=iD9_hk&vZaqmtBMTG%f(ZX`;VS+9QE!B|v=mz)HMbFSAdzT!*=(PRFQj_-z zTuHy~M2Nv{dC}EE+=x7gwi0WU{LMi@>R8cS35l+JPaT;$ZPvpM>* z0?#213FhX#Mnlaxdjg`xY?9Puo=~gPkb)(4I__FB<}1w1h^@OFWI}hDzPvQA7DStq z!;4?k5rrW9VZ0lc7{swc_;fd|Gsy@IaDCX4rN^sKG?*ms68D~P`F>lS(>Y;$R5D#* zs@%aMa|ygx(>I)!_mzvmdY|ekxRihqi0Eql+G*c0kJxg{ftG}HJ3`gdF9)P29$6cr zL}+GoSw~zynX;9yDV8gX6<5cO^TvqWr;P(Bqf4>HV>aDPO^2xntcn`ZRiTnAU|Ak} z9(^&{6x$n;3ZDwZ?((=48 zK%*AQ$jZ9Qv*QN(g5gSb1pYa5;29PtIR%d}j$badudbuu5u_9OzUXgJYdBoyxP9mR zDU2-9$3)fm@it%b(?eXih*}SaDkikkjq%+4U0GUU8mFbY?I}6yarM)>#)WYkMmX=3 zcvrCAk87;S()4MqG}o0b9A&wWU*y?^R@so=z?&_If%8AQ$IX9UU5PW-PzKi?6?>O= zhHlykz8Al+SyXSK2E{oa#P^bOe%v%7;g(X#JPSG`C6|Z1!hVZQ==S1ql(WEBa!rUy zUga}0z5J7C{qJ?GRm?$Gu}$`M`%^mhZCGnvba4PQhsJN~piWjh7=25`zTkir1-_OL zqsXYkT-sfvopsf8W=^csO0YWBa@9Ufw5K`m>+zRk(Q%T?oKP=xn2)p7>pm-~YLx_t zT}LtIKZm=n%mge>{JIAE_!2MlOzWq|ME1MEYZ5nfMU&zSDdVK-ToQ#_Lw)~TRquO6g-ItF@C!1%lU#N7B+J4x;nvB%L-Q_JXY!m%^FVquL7wP&i zK8Ph4b=;R*Rvb|_Od2fw-89qbT}I5Ba#ZUH-SzyPGy8joC;1;5BNrz;1MYEzWgcGz zr@mqCb`=CAX$~9;e#^`o5tuCIfceEC{?X^<=R1fE;WymGrI?~dv;)Ru6c<$&q_q8B zhb&nm-~@Rc^fVBc592me2IS)e0wFzI^WHohGpuS6#XKagQm=zmdt!)8oAq`{cHfWr zA$2dtwq!V>BgZiG9V#=-L?unFrXi?5lI-tWVh=Fkh7{YDCu{4#YZ?~Z7Z2|nVG~a- zNts7F*W-cVc)4O+=0mXC)sOh=k4zAw;1YrODgd&O{;MoU)10z(7cznEEI5XFXU-qy z6%t=@h3SdXd&R(3WhC#j%c7+Nd}e#=Vf*bRA2YPm8koL~l;fKoV19ymv2BLFZqJ_YP2=b9rB?nD390L=xsC&1@Y44}Yw&g9VQ}``Jt<{an#kPOietVrY_OZTXz1;*6XW4HPCy|bdd+$Opz47$84pIyhYP1 z?AhYt;Pi8H4*8!~&pRhwCRIR;s`{f^Wb8>C>|m|852UG%H)QG-I^U(dMo~DZhXSlx<`1H!*S7&P}(GjD#I$k#H=Zh*SqHf##OgnuyqA!KiZ?go}=At{= z#EU@=PBDiciHw#uLK2n|fF4){OdU4%+P4_2_^0n{OyI+de(X6W;HeJg_MhAh=XB60 z)j%I=4;V4W_BFVSru2EX+0zVhxAygPly=On@kvOL!S*qWiZ9H^cJMjh>@sqM+Z<7v zo@T%GqG3bsko0^Ttcc1V_8EnYij%3%GTgi^=92ru7m0GPmBwpZx>s?qap3X zmA}4Y6b$|m=cmqglBtw0>vf7{Fxs`h!Ol#1BAJRk@+CP$?s?IGf-Q|;Qje(P1Dw$O zw}Q*i3LY?82rf!2_NK=d>g(Wg7Usye2Em5L9+9y9=S|tZhg3z-U1x(7ZAZl^L@YdF z)e8{|X6n+alE4k5)XbhuW=%)HvMYok)Hh#6+NOosJF~$FZdaR97mh|On43^e zrIB@b z$S^z=eDc|WLTUu-|$u!c<=R{%p% zbuJjAUqQ|FS<`v!vkP@3iArd00t8IS z0nLL1bgC7lUP(}71@oRdrf#?=@PfDIF5ZhfB%;r37(hCD8sO~%nXq4D;96SCn|Y6^ zlv;%E^qHiV5k>SmzU5LOh&)3v;x3tK7aP;Hvmm6`NMjEfcAQV;X+dAX$Y9jd$Az8G z!M}uwX{2Tkq3+@?RPjVALs#khA`LbH);RW}(MI&1*M8n0M##a+-L*4-G8U^?8|LR# zZHoiZ>8+wsy$yw6Aku|z`4_9W2(x12l*y99y(NDF2FE^yCd2}NFnH-+eM9|mu2MlewaR-ohh!k@rA$TGCU-$#n9#Jw0I~_PR@TZc>?0)<9rlRm+Yks1=M1|9_5<&&6uvh$kq^^(@M^Od5S}lE$L3KD&bJ-x)G=Hy56| z3z>iOzaW~v>7U8-0*uaY7)*b(l??gxS_vD+WgT!(Lwq~hVocJ+-*`wdtI-?_dDV#LJYQE;@o!~kn`++ z(UUxR{$`I(j*_&1(LpqOETg+hSU9(pH8NZi?Ei|wGxz$cyhz*a$FgGeAJwaHnB)%G2D} z^Jx#XbJX8LaqWb)6375Sb+elcomvl;pd{Cimk>#0u#|T-g53#*Jzs~yJwLQ-Eck-8 zY3&>6`)Q0-54U@0;0aE>TgUpG0O6Juvq3WoM5BXh{>s|L;w^idg2?mkUFySdTV~Zv zpMtuF8)2z%!FnEne%^~Tj^;(-GNv~sWg7wCZxc)$(U7teSx&>IXMu!SE2WC9Rk&SZBpQ!43J2YD4pf-N!dc=N&-D7z;` zI5D+m&-^c5x4_S>y9qGG1KJ|2gW2mEIwqIS$Ds?A-&@!zTb^u`mzKlq@oPTYIJD?o z^vkg|f|m>@p@3R5Q|&++llwY8oLu(rMRmx>l2um(_vmMF7RHrGiJj1jo0FosG3j?r zp~wn2OB_RMa*nZ`>2{)vmxSObI~d0nK;^N^8SF4gLL~e*LyUUEf|pqFHyF{`nZ4WW zq*e(yQvjHbl1?=nYo$e-MdY6ywy=;ONvM4(SW~W6FPof93f1qvWA{Q36(P1J(CZTB zfS{Q9Dcy2s3`^8bFqCh<5qI!7r8jLqU8wKF`UX&$(ND1WeM9puNn1E7>M*Hp0lvAH z8ZxgI36rk)D)$)gT!U~iqONiY0*v&1AS>vYV3~k^xbpMQ>IhShkmmK5$V_%AB#K|h z!G_V8w?wrcwTdMnvCfv`m`pG@SqUBsnp_LV8J$^0o~K3j*k?D%J#Tt4(TRetF&s7f zVo@}Uk6KafZ7(9x^n5=h`U zZA2YWzTEsil-YI?GHEJop|YvUj!MLa0KT<8BQ01}h*mO|sJQ8)>g>v}88)j#k6Nr@V?dI^~~8!KO&tYF#LU=DhFp z#zh%%vuWwh74g-QM1NsTAJMwy9}VX+8vDxDl4yk`um{_Re}JF1I1Iz~AQrvjK?zHY z?B5oD)w^6D#1gaEV{jb^0zsV6UO)U74D}F{I zmwoY~MuSdo)j8-@?);|Gr0~3vaD>Uk|98*pY`Nl;vZr|;_X z2BlOm>9!y11J!i`K#>FhEhQ#_V8gj6RDBrd&C4uzvP7nCS9uBr zrua!KQX$-X1TGwh+A}jR?jj1hXxer5i=ig%^f1hDP3O3%BiE}7ZB;^3r@&eq3rHg# zG>=~Des z&WVhAOlo3Tk0C-g~rV*f0f6f1~Vj>MhfZopo-2WE) z{}?K?co{JEFASQ$4ru+iY|Ivb_Fuk0SM~rI|Hc*`0XF}Yh1|&iQK8>X0S5nuDa$@% zS{H!6f5WnWW12SrufNzIpHF3h=#U;%AQtrf6hQu;eqn+<6G+em0C3`;#;`*_A_8yz zl!ZRG!|5OYn4v6KK=OaGOprKvAS#3p3y1^#hzDf;H)8#nfZh-RiT_|wJTl+|#h<>% z6#~jM6w&|~dXA~YXC;`O(V+GHb zk69cJ2Il?q_2NIQz_tI?J3jy94|xtcc+CH92CLMw+0?FoWW;|ei;{dsefs~Rzhe^8 zFA2o{LoNTNl!pGHP$3jjK(s&9A6sjqe<@2T;9pyfvOrS6)L-y9Sc(6*{QluoMjI9e zM*aDs{STKl7XC_q$1ns>8i@TzpXPiiQva;2{vZAA@;`dQKVJAsKRXDs@fWR;2L45d zf1~?bf6?baM}=U@{GZ$8GS8IY{%?vLvc3mE`kzj6{;dEW|5cR90P+6S6F6tTQK}SK zAU4JC5v5I5iLZEe^7H>VS_EO21#vXy9YRFaou^1r|D14tHGDmm_+o_Fqf zZ~ESDxk!Q_0D>S0f`!(o*BG|@y{Pak`uex2A4f?XCBs3xG5qnLe}DViRHNTZhS6qw z5KFBj`h~To_A6VbFQb|H7x;a$yS;-y`1>21bmC#ujO(MTgMKqc{Z7A8V-LmaelqOU zy7>0XFVXb;!fJ62|IAOb(d{TNS&&IQRG3mK zRGRX~C!aHr%Y(Q!jH6mF+BrSgkK#_;jeEoBu06btFqkO0i5u-!J8njTLOhHIiH_f( zL>Ba}zD12rElCbRwA|8n!}xw!?)QcO{yx7(yJ-p>VTn;ie-ZkY9c#DtDK=#V*Y%6Y z>*ht&ZYm8_H0;oRiJJY!h@&hvWbhWubhCXs{S5S(GF6KLc)y(ti?wDm?MCXfTT#L5 zO?z@Jy%GqP_ZPuqFjH)9@pG-(O;m0-Z5<;N2Ad4yemC^rG=urK8OAru!+^?#_+?bN z#h9N(zx*2AfA-tWZ&6JqN|vzwUb)k5d@7h;Uzln-K1&#oL!gm1aW5VerWh{Vaef4TS4q1K|#VI`H-;#EA{=#P3$ z%nQ&GL_a}!K`0?3BlszS3<+cTMfB@05rGstacda;@Bil1_}SDLw6Csnt6VxD!~RVk zjLXQvsP~7%ewUqC8ajR{8Bz3&_~z*#kBa~9jXE7CaM3CBg(ds24`ntaU7Ca1RTNY9 zlBnMcf27iB&UX-IRCw=HD4KWsx3Q=oEpw3XATRR0aoz4TsbSdzSQCIQfCw+6-l$uT z2QMO$)&AwpfOm~rr&F&rKE031=2z6}4SV?g=QTVcS9ue^sDb+vIS#W>^`1LEQ`Te^8SQM)8ZtqKyw&KQN)cM&DS;VWDj0 zH2A>`!!TM^3YkpS$_m&{4*FP2H~V)zz>}H9axRYjRtsy&1wPM(pDz7Rriau10Xg=I zMfNvKfTChwF-yx!`<<()%m<63LNJ)dgFzp9WurZ)zl@G`{FWuX{t$e^3{!ROX#g3O zf2l^LozYTVjeP{TsI6 zJi~t`<1C5-if}M|8q!(CCGAs0}KjLUEKov}^D#hZal{mr;l8+EJ_jEf1T z=kcHqkrj(em7^hrTb?KIZ*-F-1lQ7(e}h)1*;!(zQNS=z|HVX3s;ZuZJ(uq(uRrkG z&6@8ILD`YJ;(0LtZ;wHy5tU+w5h{A3{HO+;I*R`N@6_nB>^1s8W{o!N*6yim*sz#1 z2K`RwyxoKyniWl*ic%J~=Ct!#@5HiJ<~P?uX)m_excrzYX~ZTOK6GMTpMQz|fAv3K z4{F2fA}}t@y@>R8yI0@{AXc8TFw^bXvu`)||B~M9R1e@{Iso@az+R?cIq4Mar3aQ$ z`9$f<;oJlgd`Fk$4n<6uv9b7C|8zb0Bz4t+t)MkOj(7&rBe~sEr92hS09s8kv0s}DJNomdKQ~W@9*+svM>ayk& zC#>*k_prdzjvyplom4%LXg`Sp!}PY@7~4Hl`?5IPklMc|GtasN7a9~kFWc^82_m?@vnaK z-y0p6j~g&n9{#;sQ2_rwx`_w1=%C$e&$f%hI2jgtwc9pUy8fU@d`ed^=B#ASl{mf3a8_T*<~Y8EY)te1o~pW#e5iWV9f)-ejO;5N#-jm6~A+ z#R3UTvk914H_0~4$Xe8?J%A0ZLBESXlAA#rE{EhgX4zm+lL-<14l|KH4s|HJd;jkJ zH&eALHMzS8n%=bU<4z6$HTrGi-Ci@k7o*%XZu3JhrEP;dS#8*lfBwS0Gp;qRO|RO- zNt=gec0ObOx6c6=v`0bzg@GdIywDDms{?z_OXBW z@V=N}haa1dO4OgUe>pb&I#B)kJeq$7|Co?axBr~CN;ee*EQ9{Wk_5ljr5Z!$q;2=$ zQ;6z={vBuU9XQs%6Y`(8JDsSm+roqnx>`blY(Wy5=Tc3Vb<6}z_%7Ud7K%Epj(So1 zs0z;zJzM~FTN^w?*q<4Y28;N<(HU{~uG`GfKsH~nMh9_{e?;}#AWGU_;;3-br#q_+ zRrU~Fi7RP<7I3Ath>sc5vmEKjp!yZ`ffO9Z4ZtTx^|(=kq9Cr(!b()FwQ7SlwHUl_ zgEn@C@Ygk?yMFJ-A>0O#=Yc|yerJ;*Trm&UxCO%THAo|4@y3wyMCn@;qC2Z7%OHP9 zl+ZuJrp&OPf5f3$=dSj^F-VW0m?+}9TPHr==t!&mg-O^}(7_-8dg;^R$m^W>Bp8Esfj5!KDNLPu{xL$)C)Z2q*pa#T#pgJB# zRn?~I?LOV998^B+z|Xy}zV2zzr5n#&!w~M!7@npdfBf{MbXYwuom39t4WAwkYQ03a z=u&!gcDlcNSV2jDH0-o{F!Ir6<}ny zn9tb#f5(Hg?0old84lXw69~?QlsY`e)-@=C#u)-vHZ1g)& zNnu@U^KiS zlq$oD)Yf4+XJbw|1Qi5(qLwo();`ELZf*i*%67*JjbmuIx;ZQ+l}Eq*X2-!*#`}}Q ziWFrLHY1k&5659PL6b*g=VC@{SIWDUs*%Gw4rsY<*;jJI(uL|+7;f~mFw_-7BTJ!C)j^>sSSdBCHz-YYtW+^7IH*EpDuqV> zjupxpEj5fDW{q#p_-`z+-CCsw7jc$`RhC3jWXMF|ZO)5%wT0B@!Dn#_qHQUPX26$p$@~h(pg@;lvX) z0k~Jg`F|YUT=$3lt3mAsYku-4N$)Ea%a@e&aXns-*L0w^G)ww=bGHX zTncGp7NP0C*J`!7fAvgZ%9d7ZylI9l^%^$Q3n6sJmOSVoTh`Xrme(>9D{o$SnCQG1 z#LaST04GANlV#Znq}-N`G3=2UA-tmj_6cOi1V9(MGXy`c(H9qwZaW$011)%+z3YoB zFztg`{0NjH5|Y{U@zLgGt-FL zCThhomYP~ed`+B5gBYxVYlDnTq>xIigVsruAc%!7D{^w!fsLGudMyBiic{SNvoltPisgTOEi8_y$G?7Q`o58el)1e~Kfq+K z$uofSP#phbjD8v~oAuSkQf5Ya99fO$8yR|KI1<+z>$OQ7Sy)(JFw2pXu?$BVOG|5+ zN$1~FRU%ht>^@H%S`FE)~wyu7~d3ZG`P&zvN5942=NtVVm zh!JVmk5s0ixvF5r9YU(GAK9WhjS+nXFDj%Re?Z!BjN>HY6QW(A@kU*q<7f9GsTPCA z&(3I1@>iGPPztn#V?v`ox4w{(yj1dQ@%-vS#$w1YVQq1~o~`1Tuo^GI%N2GVGGS?b zt~IB&vN8cjzyk<(u614o5=l2=Gr>n3Ay7SYG*#95);MCj{U~xKQUmQxZYZun>Y(}&lZv}Wno-j ztu5BV(#$N#tKwL=B>xNBhqC}y8U6`tTUl9L(zQr90r8e+)Hj?~3S0-}K03>hIdg#>1%~^i}9E?qL_2FepKe z$aU0N&$kvC-?oQFH6EJt9^*08_*#sE-f*tls9~a9D6Mi#FTm&m37}4~k=#d+&qe&H z;fiot!mnxzQ?d&298gq{qeav+XihOC0D)_IED3b9yCM|X2nYU*X+r=tf8!Y>)Y!$n zrQeH6Km^D2u7=moUPN<>kp%iEG{nmj)943YsL61g-e4T{3mGaJ^}E#`z#x&PQ5W_f z_O~V&{72LHM~aUL|E0f{W9PAflbp0)gI%|FQ(!j;m&SigID-Z#$niTudNe|Xv3Pp_ zB|RB6f++pm$%<0&x-%n3e>{bwqb077RdLjVPqQ^Fkl`i9diX#{B16Gq3meYD_%VjX zR>zBwfpWhKr%h~FCfGSnx53K!e@1O!rL-{6@ySJdX#BMyRa;pkcb+VZ7`ZuK4AQ&t ziTFsBW5(l9p3??OdXYddh=K;<9w3YfM*1%)u@DdFL9lqraHgi*e}KHOm7LmM3=4(9 zwYo<~2zI%*WKb*_(4xdfkw=bDjNUy&&r^)wzq@~LN9UmM8-;qDS3h4`%4DqPN6To@Fa#MH$}4g?mFi2#QI(OMC8*y5swf6k`R1)@jL2#2*ph8*Mn zS*3=(sphRc{A@uCb^qCY2cU%ITH;4`2Tr>icpg3 zwaR#I0Cz*zf78Zy?DM@G72=1PkEAr)2=VW6wm!z;hrEwtwK5;fD)|l5ku#igQf--1 zuZ=)Rux(88#mb=(B9f6Yy;N9P+8hn!UPY#Yi;(T~`!`O8h1TZiUco||EdVV2s7r}) zVaK|pX-yR9522LIX4jOQvtZI<1CIakKEnU7MOlYme*)2(zu>}dSm>CJGG00hOHbR~ z80SU<-jK4TqTwLnq@~be)8K;Gz^|MUzHV}OI;#iXrn=ZWH7o&jYtn>jbK8VsVpoCf z3a%uH35p{U41sZ5P5Oc3PVI(2hrm%t?gBHcZefW8sQOhAVu+iM*r*unVYp&^27scD ztH&0^e<}2Z_G`k%0+bx5L5{NGni9Lf?hQ$Ffkw=!r4{HYH^0*5lvV|nQ(6@vx1CDx ztcxLz>QY$G%@XB^{jO~$B_o!F3IOUiaYlyWRa@AutD!80J`Yt64H}&BtQ<1TSG(|C z(ca^x0i+$KD*f-GXUhouwV~iD@VMXn$7M9rfB!YSW4xV8{jPoYd}NdZrc ze-Z@x^_m$SY>8+)T?PP2P<)cwfvFfQ(3| zcCTPGL5q$7&s27E9RqhW)Q}0%LCV6QK9OV`32-&q7dsI<`R!ObMf7;n~FZnJA+tci8TNK3}`V#geP#U&;#!1xpYDOn_vZ|QID zMf661O<9sw!w3P=h6xM*>7t4XU(sjvba#Ir5sLC(aSLu7ZQyVGTmIVGztj+D{UP5Z zdJuKwzc>`t`++W;v@V7aiJ*GTf0ua}IUQ2QT090P!=V@y(%#42pjPD*uLT^bGJ zW88jYs3{g%?|STQJCW-Pm@(##wKrf#2;Hyoirkcu4p#(V?srlYTFCY3F+Iu<;ee;KLM$5C)5E?*UPvo@@Sa4b4Fltg&VIWHPLe^x<#aZ)4e zV)O@;t7zFlaOFsP<78xsLN>Ep1lW6R=qL4v^Ek{`ZHn_&p%kgZK%! zEKLT(7`BZkk##u-$P0vQGYHO*m%lfkMB@8b)iA zpaw6-v=E z^t~YgB(|ASAO{5J#5f2xu^=*3%z-f(jS#Yf6o9r&&H+3bxg5Y}Hx_I-O>^Kr6=BQz zur~`n=*~^(|4w%RA9j~c{r^&j>>%iJ3&D0e9sf%`x*=v~3+*3umg$UX(JoGBj8|-z zig#HP1m}u!qytE1e+nrOnL_Uok~oiRC3d)eYZz}qTbY#pOw~y^{@KnvDPa#mHXQ?yg2)D3oe@XaoqA#n!9P*fYYgEr?vGY1D2gHSyZS@swOsNQh1i0 zl#YAd#{lXMWF#V=_##ESIkfc+r3{$gobAv-DdRiX zc+@E~S(zU9f9goZ`5(FmH{m-&zo~Q2=^k|!0=Bm`*>ERY<_IHj{>W)YA-n283r^>bxLSOG?*6qDDq`XQ!@UPA z&5E*lHGeB@-R2{7L4?%)-BF|jAhH7J#*^+ngI($&f6%-wpg;Fxln3?8xTblh3fUo2 zHbWc`^)Z3r3?ZX$nm1nH4m0&QT)|Y_61?^A353H%hvDiT0*=%q>VV|x6@n`Mo-m6! zm>ASeXBj`|dk?~qZkrM4ViEQ0A90Xr@OO;J+(g~tO8jeZ+AN=-0>+PU6!18SZO1AZ zo)#nJe_YSZ=T{b&u}uNwCYK0P4O00&EBPef;b;gse>*HA(^L-a=H6P!+ zX#&)3RX3;X2Rns)$r2yF%S;Ne^oAlp>V2-SXdlR)`*(cIKc(~564JD!z%dfWvL(9 zqp5cH28jglf^+1M4l^;aHf+Ig7hG=j?s)af1?bVz~KX& z|6x()bq@?!XLxMV1c&P2_etm>EiEpT#XtLW`1jXuo*i0C9(oNfAC_qU1$2x};v<2K zc82qgEQ-m^D82nr?xHYcE!p)BvXNaqhe;+uZdCPgl3E?l2 zV~#`v%9@Hq^om(94Nr8o9>Z44#~(LqhnAV%0GtEG{xH~$52DP7Dr7WZ%#2W$wxv@a z(G|$6SFw$H5wg0KMaAx$wk(@bAo5Nrio0Ie^~#+t%M~Q`GMN_HnE~e5ZPmS}*vMS) z8=H0nx@g)3D+pHMe<9Sxi>zrUQhxa^K(2KMjFat=!o$tT6S3tT5w>Vctp$J&X35U$k* zkH_hozO};ve@`I<@bUW8aUDL_!z=%f)Dgp?)cZRpbJ`Spe3{K{HUP34UGo;1+^ zz%YD)n4`JkdG%MaY4puD$xtFJB+&pYZyLb zxcc^wc3kpy(2zWE)44G)Fl0VN*kN@&?fwKR7n9Lx3 zDJ-BFOuatcy;e2QtcMY?eeeOo@P>~;Y4U8Fw>f9q;>aiy)$@J+m*hI77_0?fy1kGu zUkqVmf13-L5`}4n0a~6nGBNZC6!9v0`?yNS@S1wYr;kx#vyv+Gid``HcX}pSDOTT(Em&Gsey~AICPO)s&482 zKQNbUDs0g?BwA)l8KLGz*v#fa0eZ6Exz*>mf4<78SZ83)i3`ceh+)3h(R! zg9BoM!vijIB}Sn^6-fuCB(2QV2y;&6tc09m?&_b9FSP%~iS|AEO`uF3VK4y;xK-{* zgDd4`SI#$>to}8R6i(J!YB7qq6$kKgBg(kQ`w!yBaE9Onywnco69KX=6bzo@a10N8 ze`mIs(e8DDsxFwwb%8-3mUYx%4sywyZI()7dj7S_W8w|Ibv& z(MEN5bGLN(;duXSd-o7eS0FMFc46UJe-Vz3UZFrEF;97j@%Yy%3^5|?gcN4d6Y*U) z*#!30?rL}`AUie-KDAeU4n4_S6)&{!wln(DvN;O`+IXUSt06agjeCZh;&&J^GJqHF z`h!nM?B0lHaBBwdmF0 zfW_2so_MVRx;(uBD#wDCU5tBiQy!~-OJR3>ZU;@!st9Fxqnh}anS7N@FV4;#4FzZGxWfAQUK_Ms)% z&w2;ikkD)PTk-P<`J$`P2$GFmX1qS9hIboeE0o@m7V*^CQ$9L8tz4W|jRv7DneG9~ zCj3pJNm-OxUJI^Zf_a}-*!O%+1>C!W-uPSk-Vg-e^(Ew$+yROq{w+sDJRC5{nhC*q zV@eT)U;rq~#wM|;Cp4$@fAjy`~h(Q^s3hY=>}WrJOhm zV2bOK>Qg?wr*|abXW#I$4y@kPb!sTibjt&rNSmjqzTrvcNhLfhm{cb6)03l8oH#h3 zHp~byO4*jSh)?LTe{2?FqmJ4XN0}7qA*+xe#57NA-VxFFkn|jpdS?*L6kBB80ntfC zL{!UYW(gBbB8herv6tu*Y@Ehfo9V**1&H{CyHw__z&SIGMgMz`V4Elc1w&{uX{A;Q z(ELh^6gvK$elq>=Km7PjHwtz}{%(kv5*bIW&Yy0Wq<&Lxe}Uk!(UoPsmcmMLuWR>v z6t19gSiA`ZZH328W*@#vK&l#%V**d!q)2*aD*E{E3j{>5Q~p<>`%}D!vw?JF@!wC^ z_z!io>n!u?BJB(wcF%;GD0TGARCb2KdT<#phJdr8#PQR^5JBHk(TA+@7uZl^+afcr zFg-IPXR_XVe?Xdtl1~0g*iXSDJ(ISgK7>J8vta!LNv%m$40zN*Uw$PegsMEm z7kbFyf18an9B*J0V%UNoZ4(k87>ox)u?U|k*D)XpANRW9Ekx(4-P0vLL=8N#8rmz! z=;}5}AXBIcC_BWne9oCQ}2<9-CjZ=IF+BGxR3_1)`ro)1XGqB|+RBAyk6Xtaj zZ21vp%lA+bwP#@GybP<85Ufo(I^}WB2q@WVVaSA$4rL4;7LHWnyfdhYJH`C=$us3O ze*}0e7q)?qN+2pA?28E3pcIVnd!CJsdge$O<}06Z#IVf~Gh&Q0&&6vE=v}WT3>5Y< z!W>Vke?2iVURyLb-T0QO#1M5#BC%;WJIlMH())hqy%K8@_U+{Xk33Bo>Q z&P?c3V$pN^l#-xJ!b$t|u{1h{*)Xvif8&jwe<9^HyeW7-vtU;2QF}(dVH(IoTO9UgS6xK)K^#_p&toAYMOkRd{v_cS=XVfaE^m*qz6O8@f zO+42+u-Hbs40n{!?So z-;}4mvSZXy9j61qzd83I9)5i)0NKNfCUSIV2%PpT<2D*0+{e>#Q%WPD(&|O-1)ReD zu3c;fk}^w$#Odi2u(@Y}{lWM5e+d6COQ9N4hp{J$Pmx4Y)Mp3Ykj{@4;v%TGKxKz% z<|VmDYAAH!mrZ z8R42NjIjw<`~_n%52@)(i$GaH^3SUI?x>kB^%=}8*SJEZY_{N%HBV-jJQBKkBW}xYH~^Hz=Fo1CNY5!JPN4Qcf8Fy!iby}=jK@UuC; z#z#&0bEO$0mP~%OTFrT8W5?H4tG?)duHtDi4Je?@KXsH}VG0L^w0 zQYK`W=v<<92T~JOTl%WAPm9{))99TpK

qUqsk=14&@Kz?|iB#o8Ddw-jc55Z8}c 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 z zu{M8;>c$BFfxiE_nyRpb&xZC+%!BFePxTQkwR8jnZt7$Jkp=`cKgB^+fko94Pp^RD z(K_-1#^xD$L~S@5A!p#R!E!z*DpfOI37uUwq^e(gX;$!_5+&aHtsAQ%@i+6&a?OyG z$`R$;q9R59Z%EN1e!ZbV!Nd{8aNGz#WM$mi98POJ4xCaG#8%u=VnxJbfw_BsMwGiM z8zFBbT6%lW+8NM0pN?ut*^Sh`y z6g=es@FQauvmQ@m;qbsDi|RxIk_0nsIQy?MY*UjNW@bnVZMBMz^p6U>Vk)LACU6o{ zJk-8dwen}vAre@68Xe^iQ2~^2&&ih?YesOI2Jvf zNnV*+g@l<~9@&AtBTDP@rrZwhLla?|dT1T!xmU@4yue%ef%nx5^HfsfJ7@{nrn3Ps zb1v_3<7TjHO?8N*d7hg}VCk&RhqUp)pSWjbXVNtrIo=SUxZ#}}_J)zgji3^hM20~p zPDmDFK#LWtLmGGK3^eIOB3B4UB1KJIrDj>xvH_JNQq&L5^{QwNeE*YAy1uFe-)+QQ zTFb4`nL!|}T#SVoU$e25Y)^^eH<71e(B#u?<~}`9fVF?H`W4L2pTzMo!*$XQFiWd- zWbFAUbg|bR_fh#x#6@;!D4y_=I51a(1KZGsp(+yQEwhx@yCAL)D|$w!J%+SjS17%b z6q6SekZ3xunXx|`Hr<0T>`HwGo4Vw_w<1}ejZRqF4B7Q(!pz!DXF_PZ@!v50ta;hs z30vk%s#tajX021(u(__IsXVjtu&AabJIUTA+vneN`&CWBJzB*g7(F{NK68Wme$y$3 z4AqX`u8a{gt!IcB)XN_ob)wIf@>BO8N8!6 zvU5k7=?Zcc+9%70C}GA~D^*F2P$*#|*;?Adto2E_;fmSDKw124O_#9yVr4-~6u|6tvnudG~mRV)Z774$nm^CHeQVI3#^LF zV#vzSJz2iDKV-s6fkSjiaeH30tYxF)@C|cg7fwq1`s_qf+?>JyYW1NdQXGAu6 zo5X>88|gxL4nZw6V9Zz{fN*Z?!c2$V8ZC&^oob4AAPkNE5E5 zw>|;^D1$EiouL{#bk-r&6O+Eg3i+A97DuDK=bK_UOjEcmX`I&)bwysEtGARSBA1)_ zwA3CEm&v#(E5oX-nEP!rK&eQOz-f?`F^Bx4DA`c4}-f6e*`W^ye zkrVJDLqZ|EK4<0sJaHxN$OaekMH2vPTMugscq8*i0JBsBIR6alEvf;~Fs~zrMnD$y z>j=Fa5DWE6dl|9U&vgSz{)$Em0Q&lh0YDz`75yWEUqqPt&|!cH}AASzjX;1`Ns=={qil~^Ir>-2SD^|8sL`^ zJ9u6C#UDKoIPzEbTALCQ5T>3E4tVrWG zd?0=8Zh;W+EfD3;9R2^<178ROXK@W0Z0b;Q+J2_bXcO?qaFCG}D|DoZ(Mz)c7(YDS1 z)v~StV8GD}0BCTj#J@6Y`GcD;|AQlfpZ^V^NWMUJ>wh7#?H5x8$$zC}{0Gwf`45E1 z_+Q!R^&nD(x_g1A%bhNa=soob{)_Y5*|0%*#r7fsh&gyXL)k5!8j39IpQ-4;?U? z43OhBSwo28H)Jm>D*xs9zl{k%{NtM(94Paz#=ywfApL&vMfpF}E7U(!BJiXP5a+d| z{gY~o4pt9%REEN O0m)!cgn=*f_WuBKU42~u diff --git a/dist/extension/birb.js b/dist/extension/birb.js index f3319da..152e06e 100644 --- a/dist/extension/birb.js +++ b/dist/extension/birb.js @@ -847,6 +847,7 @@ const SAVE_KEY = "birbSaveData"; const ROOT_PATH = ""; + const SET_CONTEXT = "__CONTEXT__"; /** * @typedef {import('./application.js').BirbSaveData} BirbSaveData @@ -1033,7 +1034,7 @@ */ isContextActive() { // @ts-expect-error - return typeof chrome !== "undefined"; + return typeof chrome !== "undefined" && typeof chrome.storage !== "undefined" && typeof chrome.storage.sync !== "undefined"; } /** @@ -1077,6 +1078,7 @@ } class ObsidianContext extends Context { + /** * @override * @returns {boolean} @@ -1164,15 +1166,29 @@ } } - const CONTEXTS = [ + const contextProcessingOrder = [ new UserScriptContext(), new ObsidianContext(), new BrowserExtensionContext(), new LocalContext() ]; + const CONTEXTS_BY_KEY = { + "local": LocalContext, + "userscript": UserScriptContext, + "browser-extension": BrowserExtensionContext, + "obsidian": ObsidianContext + }; + + /** + * Determines and returns the current context + * @returns {Context} + */ function getContext() { - for (const context of CONTEXTS) { + if (CONTEXTS_BY_KEY[SET_CONTEXT]) { + return new CONTEXTS_BY_KEY[SET_CONTEXT](); + } + for (const context of contextProcessingOrder) { if (context.isContextActive()) { return context; } diff --git a/dist/obsidian/main.js b/dist/obsidian/main.js index 5cf3f53..9d2be02 100644 --- a/dist/obsidian/main.js +++ b/dist/obsidian/main.js @@ -852,6 +852,7 @@ module.exports = class PocketBird extends Plugin { const SAVE_KEY = "birbSaveData"; const ROOT_PATH = ""; + const SET_CONTEXT = "__CONTEXT__"; /** * @typedef {import('./application.js').BirbSaveData} BirbSaveData @@ -1038,7 +1039,7 @@ module.exports = class PocketBird extends Plugin { */ isContextActive() { // @ts-expect-error - return typeof chrome !== "undefined"; + return typeof chrome !== "undefined" && typeof chrome.storage !== "undefined" && typeof chrome.storage.sync !== "undefined"; } /** @@ -1082,6 +1083,7 @@ module.exports = class PocketBird extends Plugin { } class ObsidianContext extends Context { + /** * @override * @returns {boolean} @@ -1169,15 +1171,29 @@ module.exports = class PocketBird extends Plugin { } } - const CONTEXTS = [ + const contextProcessingOrder = [ new UserScriptContext(), new ObsidianContext(), new BrowserExtensionContext(), new LocalContext() ]; + const CONTEXTS_BY_KEY = { + "local": LocalContext, + "userscript": UserScriptContext, + "browser-extension": BrowserExtensionContext, + "obsidian": ObsidianContext + }; + + /** + * Determines and returns the current context + * @returns {Context} + */ function getContext() { - for (const context of CONTEXTS) { + if (CONTEXTS_BY_KEY[SET_CONTEXT]) { + return new CONTEXTS_BY_KEY[SET_CONTEXT](); + } + for (const context of contextProcessingOrder) { if (context.isContextActive()) { return context; } diff --git a/dist/userscript/birb.user.js b/dist/userscript/birb.user.js index 2614285..4e5b151 100644 --- a/dist/userscript/birb.user.js +++ b/dist/userscript/birb.user.js @@ -861,6 +861,7 @@ const SAVE_KEY = "birbSaveData"; const ROOT_PATH = ""; + const SET_CONTEXT = "__CONTEXT__"; /** * @typedef {import('./application.js').BirbSaveData} BirbSaveData @@ -1047,7 +1048,7 @@ */ isContextActive() { // @ts-expect-error - return typeof chrome !== "undefined"; + return typeof chrome !== "undefined" && typeof chrome.storage !== "undefined" && typeof chrome.storage.sync !== "undefined"; } /** @@ -1091,6 +1092,7 @@ } class ObsidianContext extends Context { + /** * @override * @returns {boolean} @@ -1178,15 +1180,29 @@ } } - const CONTEXTS = [ + const contextProcessingOrder = [ new UserScriptContext(), new ObsidianContext(), new BrowserExtensionContext(), new LocalContext() ]; + const CONTEXTS_BY_KEY = { + "local": LocalContext, + "userscript": UserScriptContext, + "browser-extension": BrowserExtensionContext, + "obsidian": ObsidianContext + }; + + /** + * Determines and returns the current context + * @returns {Context} + */ function getContext() { - for (const context of CONTEXTS) { + if (CONTEXTS_BY_KEY[SET_CONTEXT]) { + return new CONTEXTS_BY_KEY[SET_CONTEXT](); + } + for (const context of contextProcessingOrder) { if (context.isContextActive()) { return context; } diff --git a/dist/vencord/birb.export.js b/dist/vencord/birb.export.js new file mode 100644 index 0000000..1022973 --- /dev/null +++ b/dist/vencord/birb.export.js @@ -0,0 +1,2837 @@ +export const Birb = () => { +(function () { + 'use strict'; + + const Directions = { + LEFT: -1, + RIGHT: 1, + }; + + let debugMode = location.hostname === "127.0.0.1"; + + /** + * @returns {boolean} Whether debug mode is enabled + */ + function isDebug() { + return debugMode; + } + + /** + * @param {boolean} value + */ + function setDebug(value) { + debugMode = value; + } + + /** + * Create an HTML element with the specified parameters + * @param {string} className + * @param {string} [textContent] + * @param {string} [id] + * @returns {HTMLElement} + */ + function makeElement(className, textContent, id) { + const element = document.createElement("div"); + element.classList.add(className); + if (textContent) { + element.textContent = textContent; + } + if (id) { + element.id = id; + } + return element; + } + + /** + * @param {Document|Element} element + * @param {(e: Event) => void} action + */ + function onClick(element, action) { + element.addEventListener("click", (e) => action(e)); + element.addEventListener("touchend", (e) => { + if (e instanceof TouchEvent === false) { + return; + } else if (element instanceof HTMLElement === false) { + return; + } + const touch = e.changedTouches[0]; + const rect = element.getBoundingClientRect(); + if ( + touch.clientX >= rect.left && + touch.clientX <= rect.right && + touch.clientY >= rect.top && + touch.clientY <= rect.bottom + ) { + action(e); + } + }); + } + + /** + * @param {HTMLElement|null} element The element to detect drag events on + * @param {boolean} [parent] Whether to move the parent element when the child is dragged + * @param {(top: number, left: number) => void} [callback] Callback for when element is moved + * @param {HTMLElement} [pageElement] The page element to constrain movement within + */ + function makeDraggable(element, parent = true, callback = () => { }, pageElement) { + if (!element) { + return; + } + + let isMouseDown = false; + let offsetX = 0; + let offsetY = 0; + let elementToMove = parent ? element.parentElement : element; + + if (!elementToMove) { + error("Birb: Parent element not found"); + return; + } + + element.addEventListener("mousedown", (e) => { + isMouseDown = true; + offsetX = e.clientX - elementToMove.offsetLeft; + offsetY = e.clientY - elementToMove.offsetTop; + }); + + element.addEventListener("touchstart", (e) => { + isMouseDown = true; + const touch = e.touches[0]; + offsetX = touch.clientX - elementToMove.offsetLeft; + offsetY = touch.clientY - elementToMove.offsetTop; + e.preventDefault(); + e.stopPropagation(); + }); + + document.addEventListener("mouseup", (e) => { + if (isMouseDown) { + callback(elementToMove.offsetTop, elementToMove.offsetLeft); + e.preventDefault(); + } + isMouseDown = false; + }); + + document.addEventListener("touchend", (e) => { + if (isMouseDown) { + callback(elementToMove.offsetTop, elementToMove.offsetLeft); + e.preventDefault(); + } + isMouseDown = false; + }); + + document.addEventListener("mousemove", (e) => { + const page = pageElement || document.documentElement; + const maxX = page.scrollWidth - elementToMove.clientWidth; + const maxY = page.scrollHeight - elementToMove.clientHeight; + if (isMouseDown) { + elementToMove.style.left = `${Math.max(0, Math.min(maxX, e.clientX - offsetX))}px`; + elementToMove.style.top = `${Math.max(0, Math.min(maxY, e.clientY - offsetY))}px`; + } + }); + + document.addEventListener("touchmove", (e) => { + if (isMouseDown) { + const touch = e.touches[0]; + elementToMove.style.left = `${Math.max(0, touch.clientX - offsetX)}px`; + elementToMove.style.top = `${Math.max(0, touch.clientY - offsetY)}px`; + } + }); + } + + /** + * @param {() => void} func + * @param {Element} [closeButton] + * @param {boolean} [allowEscape] Whether to allow closing with the Escape key + */ + function makeClosable(func, closeButton, allowEscape = true) { + if (closeButton) { + onClick(closeButton, func); + } + document.addEventListener("keydown", (e) => { + if (closeButton && !document.body.contains(closeButton)) { + return; + } + if (allowEscape && e.key === "Escape") { + func(); + } + }); + } + + /** + * @returns {boolean} Whether the user is on a mobile device + */ + function isMobile() { + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); + } + + function log() { + console.log("Birb: ", ...arguments); + } + + function debug() { + if (isDebug()) { + console.debug("Birb: ", ...arguments); + } + } + + function error() { + console.error("Birb: ", ...arguments); + } + + /** + * Get a layer from a sprite sheet array + * @param {string[][]} spriteSheet The sprite sheet pixel array + * @param {number} spriteIndex The sprite index + * @param {number} width The width of each sprite + * @returns {string[][]} + */ + function getLayer(spriteSheet, spriteIndex, width) { + // From an array of a horizontal sprite sheet, get the layer for a specific sprite + const layer = []; + for (let y = 0; y < width; y++) { + layer.push(spriteSheet[y].slice(spriteIndex * width, (spriteIndex + 1) * width)); + } + return layer; + } + + /** + * The height of the inner browser window + * Will be the same as getFixedWindowHeight() on most browsers + * On iOS, it will vary to be the height excluding the current address bar size (potentially greater than fixed height) + */ + function getWindowHeight() { + // Necessary because iOS 26 Safari is terrible and won't render + // fixed/sticky elements behind the address bar + return window.innerHeight; + } + + /** + * The fixed height of the inner browser window + * Will be the same as getWindowHeight() on most browsers + * On iOS, it will always be the height of the window when the address bar is fully expanded + * @returns The true height of the inner browser window + */ + function getFixedWindowHeight() { + return document.documentElement.clientHeight; + } + + /** Indicators for parts of the base bird sprite sheet */ + const Sprite = { + THEME_HIGHLIGHT: "theme-highlight", + TRANSPARENT: "transparent", + OUTLINE: "outline", + BORDER: "border", + FOOT: "foot", + BEAK: "beak", + EYE: "eye", + FACE: "face", + HOOD: "hood", + NOSE: "nose", + BELLY: "belly", + UNDERBELLY: "underbelly", + WING: "wing", + WING_EDGE: "wing-edge", + HEART: "heart", + HEART_BORDER: "heart-border", + HEART_SHINE: "heart-shine", + FEATHER_SPINE: "feather-spine", + }; + + /** @type {Record} */ + const SPRITE_SHEET_COLOR_MAP = { + "transparent": Sprite.TRANSPARENT, + "#ffffff": Sprite.BORDER, + "#000000": Sprite.OUTLINE, + "#010a19": Sprite.BEAK, + "#190301": Sprite.EYE, + "#af8e75": Sprite.FOOT, + "#639bff": Sprite.FACE, + "#99e550": Sprite.HOOD, + "#d95763": Sprite.NOSE, + "#f8b143": Sprite.BELLY, + "#ec8637": Sprite.UNDERBELLY, + "#578ae6": Sprite.WING, + "#326ed9": Sprite.WING_EDGE, + "#c82e2e": Sprite.HEART, + "#501a1a": Sprite.HEART_BORDER, + "#ff6b6b": Sprite.HEART_SHINE, + "#373737": Sprite.FEATHER_SPINE, + }; + + class BirdType { + /** + * @param {string} name + * @param {string} description + * @param {Record} colors + * @param {string[]} [tags] + */ + constructor(name, description, colors, tags = []) { + this.name = name; + this.description = description; + const defaultColors = { + [Sprite.TRANSPARENT]: "transparent", + [Sprite.OUTLINE]: "#000000", + [Sprite.BORDER]: "#ffffff", + [Sprite.BEAK]: "#000000", + [Sprite.EYE]: "#000000", + [Sprite.HEART]: "#c82e2e", + [Sprite.HEART_BORDER]: "#501a1a", + [Sprite.HEART_SHINE]: "#ff6b6b", + [Sprite.FEATHER_SPINE]: "#373737", + [Sprite.HOOD]: colors.face, + [Sprite.NOSE]: colors.face, + }; + /** @type {Record} */ + this.colors = { ...defaultColors, ...colors, [Sprite.THEME_HIGHLIGHT]: colors[Sprite.THEME_HIGHLIGHT] ?? colors.hood ?? colors.face }; + this.tags = tags; + } + } + + /** @type {Record} */ + const SPECIES = { + bluebird: new BirdType("Eastern Bluebird", + "Native to North American and very social, though can be timid around people.", { + [Sprite.FOOT]: "#af8e75", + [Sprite.FACE]: "#639bff", + [Sprite.BELLY]: "#f8b143", + [Sprite.UNDERBELLY]: "#ec8637", + [Sprite.WING]: "#578ae6", + [Sprite.WING_EDGE]: "#326ed9", + }), + shimaEnaga: new BirdType("Shima Enaga", + "Small, fluffy birds found in the snowy regions of Japan, these birds are highly sought after by ornithologists and nature photographers.", { + [Sprite.FOOT]: "#af8e75", + [Sprite.FACE]: "#ffffff", + [Sprite.BELLY]: "#ebe9e8", + [Sprite.UNDERBELLY]: "#ebd9d0", + [Sprite.WING]: "#f3d3c1", + [Sprite.WING_EDGE]: "#2d2d2dff", + [Sprite.THEME_HIGHLIGHT]: "#d7ac93", + }), + tuftedTitmouse: new BirdType("Tufted Titmouse", + "Native to the eastern United States, full of personality, and notably my wife's favorite bird.", { + [Sprite.FOOT]: "#af8e75", + [Sprite.FACE]: "#c7cad7", + [Sprite.BELLY]: "#e4e5eb", + [Sprite.UNDERBELLY]: "#d7cfcb", + [Sprite.WING]: "#b1b5c5", + [Sprite.WING_EDGE]: "#9d9fa9", + }, ["tuft"]), + europeanRobin: new BirdType("European Robin", + "Native to western Europe, this is the quintessential robin. Quite friendly, you'll often find them searching for worms.", { + [Sprite.FOOT]: "#af8e75", + [Sprite.FACE]: "#ffaf34", + [Sprite.HOOD]: "#aaa094", + [Sprite.BELLY]: "#ffaf34", + [Sprite.UNDERBELLY]: "#babec2", + [Sprite.WING]: "#aaa094", + [Sprite.WING_EDGE]: "#888580", + [Sprite.THEME_HIGHLIGHT]: "#ffaf34", + }), + redCardinal: new BirdType("Red Cardinal", + "Native to the eastern United States, this strikingly red bird is hard to miss.", { + [Sprite.BEAK]: "#d93619", + [Sprite.FOOT]: "#af8e75", + [Sprite.FACE]: "#31353d", + [Sprite.HOOD]: "#e83a1b", + [Sprite.BELLY]: "#e83a1b", + [Sprite.UNDERBELLY]: "#dc3719", + [Sprite.WING]: "#d23215", + [Sprite.WING_EDGE]: "#b1321c", + }, ["tuft"]), + americanGoldfinch: new BirdType("American Goldfinch", + "Coloured a brilliant yellow, this bird feeds almost entirely on the seeds of plants such as thistle, sunflowers, and coneflowers.", { + [Sprite.BEAK]: "#ffaf34", + [Sprite.FOOT]: "#af8e75", + [Sprite.FACE]: "#fff255", + [Sprite.NOSE]: "#383838", + [Sprite.HOOD]: "#383838", + [Sprite.BELLY]: "#fff255", + [Sprite.UNDERBELLY]: "#f5ea63", + [Sprite.WING]: "#e8e079", + [Sprite.WING_EDGE]: "#191919", + [Sprite.THEME_HIGHLIGHT]: "#ffcc00" + }), + barnSwallow: new BirdType("Barn Swallow", + "Agile birds that often roost in man-made structures, these birds are known to build nests near Ospreys for protection.", { + [Sprite.FOOT]: "#af8e75", + [Sprite.FACE]: "#db7c4d", + [Sprite.BELLY]: "#f7e1c9", + [Sprite.UNDERBELLY]: "#ebc9a3", + [Sprite.WING]: "#2252a9", + [Sprite.WING_EDGE]: "#1c448b", + [Sprite.HOOD]: "#2252a9", + }), + mistletoebird: new BirdType("Mistletoebird", + "Native to Australia, these birds eat mainly mistletoe and in turn spread the seeds far and wide.", { + [Sprite.FOOT]: "#6c6a7c", + [Sprite.FACE]: "#352e6d", + [Sprite.BELLY]: "#fd6833", + [Sprite.UNDERBELLY]: "#e6e1d8", + [Sprite.WING]: "#342b7c", + [Sprite.WING_EDGE]: "#282065", + }), + redAvadavat: new BirdType("Red Avadavat", + "Native to India and southeast Asia, these birds are also known as Strawberry Finches due to their speckled plumage.", { + [Sprite.BEAK]: "#f71919", + [Sprite.FOOT]: "#af7575", + [Sprite.FACE]: "#cb092b", + [Sprite.BELLY]: "#ae1724", + [Sprite.UNDERBELLY]: "#831b24", + [Sprite.WING]: "#7e3030", + [Sprite.WING_EDGE]: "#490f0f", + }), + scarletRobin: new BirdType("Scarlet Robin", + "Native to Australia, this striking robin can be found in Eucalyptus forests.", { + [Sprite.FOOT]: "#494949", + [Sprite.FACE]: "#3d3d3d", + [Sprite.BELLY]: "#fc5633", + [Sprite.UNDERBELLY]: "#dcdcdc", + [Sprite.WING]: "#2b2b2b", + [Sprite.WING_EDGE]: "#ebebeb", + [Sprite.THEME_HIGHLIGHT]: "#fc5633", + }), + americanRobin: new BirdType("American Robin", + "While not a true robin, this social North American bird is so named due to its orange coloring. It seems unbothered by nearby humans.", { + [Sprite.BEAK]: "#e89f30", + [Sprite.FOOT]: "#9f8075", + [Sprite.FACE]: "#2d2d2d", + [Sprite.BELLY]: "#eb7a3a", + [Sprite.UNDERBELLY]: "#eb7a3a", + [Sprite.WING]: "#444444", + [Sprite.WING_EDGE]: "#232323", + [Sprite.THEME_HIGHLIGHT]: "#eb7a3a", + }), + carolinaWren: new BirdType("Carolina Wren", + "Native to the eastern United States, these little birds are known for their curious and energetic nature.", { + [Sprite.FOOT]: "#af8e75", + [Sprite.FACE]: "#edc7a9", + [Sprite.NOSE]: "#f7eee5", + [Sprite.HOOD]: "#c58a5b", + [Sprite.BELLY]: "#e1b796", + [Sprite.UNDERBELLY]: "#c79e7c", + [Sprite.WING]: "#c58a5b", + [Sprite.WING_EDGE]: "#866348", + }), + }; + + class Layer { + /** + * @param {string[][]} pixels + * @param {string} [tag] + */ + constructor(pixels, tag = "default") { + this.pixels = pixels; + this.tag = tag; + } + } + + class Frame { + + /** @type {{ [tag: string]: string[][] }} */ + #pixelsByTag = {}; + + /** + * @param {Layer[]} layers + */ + constructor(layers) { + /** @type {Set} */ + let tags = new Set(); + for (let layer of layers) { + tags.add(layer.tag); + } + tags.add("default"); + for (let tag of tags) { + let maxHeight = layers.reduce((max, layer) => Math.max(max, layer.pixels.length), 0); + if (layers[0].tag !== "default") { + throw new Error("First layer must have the 'default' tag"); + } + this.pixels = layers[0].pixels.map(row => row.slice()); + // Pad from top with transparent pixels + while (this.pixels.length < maxHeight) { + this.pixels.unshift(new Array(this.pixels[0].length).fill(Sprite.TRANSPARENT)); + } + // Combine layers + for (let i = 1; i < layers.length; i++) { + if (layers[i].tag === "default" || layers[i].tag === tag) { + let layerPixels = layers[i].pixels; + let topMargin = maxHeight - layerPixels.length; + for (let y = 0; y < layerPixels.length; y++) { + for (let x = 0; x < layerPixels[y].length; x++) { + this.pixels[y + topMargin][x] = layerPixels[y][x] !== Sprite.TRANSPARENT ? layerPixels[y][x] : this.pixels[y + topMargin][x]; + } + } + } + } + this.#pixelsByTag[tag] = this.pixels.map(row => row.slice()); + } + } + + /** + * @param {string} [tag] + * @returns {string[][]} + */ + getPixels(tag = "default") { + return this.#pixelsByTag[tag] ?? this.#pixelsByTag["default"]; + } + + /** + * @param {CanvasRenderingContext2D} ctx + * @param {BirdType} [species] + * @param {number} direction + * @param {number} canvasPixelSize + */ + draw(ctx, direction, canvasPixelSize, species) { + // Clear the canvas before drawing the new frame + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + + const pixels = this.getPixels(species?.tags[0]); + for (let y = 0; y < pixels.length; y++) { + const row = pixels[y]; + for (let x = 0; x < pixels[y].length; x++) { + const cell = direction === Directions.LEFT ? row[x] : row[pixels[y].length - x - 1]; + ctx.fillStyle = species?.colors[cell] ?? cell; + ctx.fillRect(x * canvasPixelSize, y * canvasPixelSize, canvasPixelSize, canvasPixelSize); + } } } + } + + class Anim { + /** + * @param {Frame[]} frames + * @param {number[]} durations + * @param {boolean} loop + */ + constructor(frames, durations, loop = true) { + this.frames = frames; + this.durations = durations; + this.loop = loop; + this.lastFrameIndex = -1; + this.lastDirection = null; + this.lastTimeStart = null; + } + + getAnimationDuration() { + return this.durations.reduce((a, b) => a + b, 0); + } + + /** + * Get the current frame index based on elapsed time + * @param {number} time The elapsed time since animation start + * @returns {number} The index of the current frame + */ + getCurrentFrameIndex(time) { + let totalDuration = 0; + for (let i = 0; i < this.durations.length; i++) { + totalDuration += this.durations[i]; + if (time < totalDuration) { + return i; + } + } + return this.frames.length - 1; + } + + /** + * Clear the cached frame state + */ + #clearCache() { + this.lastFrameIndex = -1; + this.lastDirection = null; + } + + /** + * Check if the frame needs to be redrawn + * @param {number} frameIndex The current frame index + * @param {number} direction The current direction + * @returns {boolean} Whether the frame needs to be redrawn + */ + #shouldRedraw(frameIndex, direction) { + return frameIndex !== this.lastFrameIndex || direction !== this.lastDirection; + } + + /** + * @param {CanvasRenderingContext2D} ctx + * @param {number} direction + * @param {number} timeStart The start time of the animation in milliseconds + * @param {number} canvasPixelSize The size of a canvas pixel in pixels + * @param {BirdType} [species] The species to use for the animation + * @returns {boolean} Whether the animation is complete + */ + draw(ctx, direction, timeStart, canvasPixelSize, species) { + // Reset cache if animation was restarted + if (this.lastTimeStart !== timeStart) { + this.#clearCache(); + this.lastTimeStart = timeStart; + } + + let time = Date.now() - timeStart; + const duration = this.getAnimationDuration(); + + if (this.loop) { + time %= duration; + } + + const currentFrameIndex = this.getCurrentFrameIndex(time); + + if (this.#shouldRedraw(currentFrameIndex, direction)) { + this.frames[currentFrameIndex].draw(ctx, direction, canvasPixelSize, species); + this.lastFrameIndex = currentFrameIndex; + this.lastDirection = direction; + } + + // Return whether animation is complete (for non-looping animations) + return !this.loop && time >= duration; + } + } + + /** + * @typedef {keyof typeof Animations} AnimationType + */ + + const Animations = /** @type {const} */ ({ + STILL: "STILL", + BOB: "BOB", + FLYING: "FLYING", + HEART: "HEART" + }); + + class Birb { + animStart = Date.now(); + x = 0; + y = 0; + direction = Directions.RIGHT; + isAbsolutePositioned = false; + visible = true; + /** @type {AnimationType} */ + currentAnimation = Animations.STILL; + + /** + * @param {number} birbCssScale + * @param {number} canvasPixelSize + * @param {string[][]} spriteSheet The loaded sprite sheet pixel data + * @param {number} spriteWidth + * @param {number} spriteHeight + */ + constructor(birbCssScale, canvasPixelSize, spriteSheet, spriteWidth, spriteHeight) { + this.birbCssScale = birbCssScale; + this.canvasPixelSize = canvasPixelSize; + this.windowPixelSize = canvasPixelSize * birbCssScale; + this.spriteWidth = spriteWidth; + this.spriteHeight = spriteHeight; + + // Build layers from sprite sheet + this.layers = { + base: new Layer(getLayer(spriteSheet, 0, this.spriteWidth)), + down: new Layer(getLayer(spriteSheet, 1, this.spriteWidth)), + heartOne: new Layer(getLayer(spriteSheet, 2, this.spriteWidth)), + heartTwo: new Layer(getLayer(spriteSheet, 3, this.spriteWidth)), + heartThree: new Layer(getLayer(spriteSheet, 4, this.spriteWidth)), + tuftBase: new Layer(getLayer(spriteSheet, 5, this.spriteWidth), "tuft"), + tuftDown: new Layer(getLayer(spriteSheet, 6, this.spriteWidth), "tuft"), + wingsUp: new Layer(getLayer(spriteSheet, 7, this.spriteWidth)), + wingsDown: new Layer(getLayer(spriteSheet, 8, this.spriteWidth)), + happyEye: new Layer(getLayer(spriteSheet, 9, this.spriteWidth)), + }; + + // Build frames from layers + this.frames = { + base: new Frame([this.layers.base, this.layers.tuftBase]), + headDown: new Frame([this.layers.down, this.layers.tuftDown]), + wingsDown: new Frame([this.layers.base, this.layers.tuftBase, this.layers.wingsDown]), + wingsUp: new Frame([this.layers.down, this.layers.tuftDown, this.layers.wingsUp]), + heartOne: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartOne]), + heartTwo: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartTwo]), + heartThree: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartThree]), + heartFour: new Frame([this.layers.base, this.layers.tuftBase, this.layers.happyEye, this.layers.heartTwo]), + }; + + // Build animations from frames + this.animations = { + [Animations.STILL]: new Anim([this.frames.base], [1000]), + [Animations.BOB]: new Anim([ + this.frames.base, + this.frames.headDown + ], [ + 420, + 420 + ]), + [Animations.FLYING]: new Anim([ + this.frames.base, + this.frames.wingsUp, + this.frames.headDown, + this.frames.wingsDown, + ], [ + 30, + 80, + 30, + 60, + ]), + [Animations.HEART]: new Anim([ + this.frames.heartOne, + this.frames.heartTwo, + this.frames.heartThree, + this.frames.heartFour, + this.frames.heartThree, + this.frames.heartFour, + this.frames.heartThree, + this.frames.heartFour, + ], [ + 60, + 80, + 250, + 250, + 250, + 250, + 250, + 250, + ], false), + }; + + // Create canvas element + this.canvas = document.createElement("canvas"); + this.canvas.id = "birb"; + this.canvas.width = this.frames.base.getPixels()[0].length * canvasPixelSize; + this.canvas.height = spriteHeight * canvasPixelSize; + + this.ctx = this.canvas.getContext("2d"); + + // Append to document + document.body.appendChild(this.canvas); + } + + /** + * Draw the current animation frame + * @param {BirdType} species The species color data + * @returns {boolean} Whether the animation has completed (for non-looping animations) + */ + draw(species) { + const anim = this.animations[this.currentAnimation]; + return anim.draw(this.ctx, this.direction, this.animStart, this.canvasPixelSize, species); + } + + /** + * @returns {AnimationType} The current animation key + */ + getCurrentAnimation() { + return this.currentAnimation; + } + + /** + * Set the current animation by name and reset the animation timer + * @param {AnimationType} animationName + */ + setAnimation(animationName) { + this.currentAnimation = animationName; + this.animStart = Date.now(); + } + + /** + * Get the frames object + * @returns {Record} + */ + getFrames() { + return this.frames; + } + + /** + * Get the canvas element + * @returns {HTMLCanvasElement} + */ + getElement() { + return this.canvas; + } + + /** + * Get the canvas width in CSS pixels + * @returns {number} + */ + getElementWidth() { + return this.canvas.width * this.birbCssScale; + } + + /** + * Get the canvas height in CSS pixels + * @returns {number} + */ + getElementHeight() { + return this.canvas.height * this.birbCssScale; + } + + getElementTop() { + const rect = this.canvas.getBoundingClientRect(); + return rect.top; + } + + /** + * Set the X position + * @param {number} x + */ + setX(x) { + this.x = x; + let mod = this.getElementWidth() / -2 - (this.windowPixelSize * (this.direction === Directions.RIGHT ? 2 : -2)); + this.canvas.style.left = `${x + mod}px`; + } + + /** + * Set the Y position + * @param {number} y + */ + setY(y) { + this.y = y; + let bottom; + if (this.isAbsolutePositioned) { + // Position is absolute, convert from fixed + // Account for address bar shrinkage on iOS + bottom = y - window.scrollY - (getWindowHeight() - getFixedWindowHeight()); + } else { + // Position is fixed + bottom = y; + } + this.canvas.style.bottom = `${bottom}px`; + } + + /** + * Get the current X position + * @returns {number} + */ + getX() { + return this.x; + } + + /** + * Get the current Y position + * @returns {number} + */ + getY() { + return this.y; + } + + /** + * Set the direction the bird is facing + * @param {number} direction + */ + setDirection(direction) { + this.direction = direction; + } + + /** + * Set whether the element should be absolutely positioned + * @param {boolean} absolute + */ + setAbsolutePositioned(absolute) { + this.isAbsolutePositioned = absolute; + if (absolute) { + this.canvas.classList.add("birb-absolute"); + } else { + this.canvas.classList.remove("birb-absolute"); + } + // Update Y position to apply the new positioning mode + this.setY(this.y); + } + + /** + * Set visibility of the bird + * @param {boolean} visible + */ + setVisible(visible) { + this.visible = visible; + this.canvas.style.display = visible ? "" : "none"; + } + + /** + * Get visibility of the bird + * @returns {boolean} + */ + isVisible() { + return this.visible; + } + } + + const SAVE_KEY = "birbSaveData"; + const ROOT_PATH = ""; + const SET_CONTEXT = "local"; + + /** + * @typedef {import('./application.js').BirbSaveData} BirbSaveData + */ + + /** + * @abstract + */ + class Context { + + /** + * @abstract + * @returns {boolean} Whether this context is applicable + */ + isContextActive() { + throw new Error("Method not implemented"); + } + + /** + * @abstract + * @returns {Promise} + */ + async getSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @abstract + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + throw new Error("Method not implemented"); + } + + /** + * @abstract + */ + resetSaveData() { + throw new Error("Method not implemented"); + } + + /** + * @returns {string[]} A list of CSS selectors for focusable elements + */ + getFocusableElements() { + return ["img", "video", ".birb-sticky-note"]; + } + + getFocusElementTopMargin() { + return 80; + } + + /** + * @returns {string} The current path of the active page in this context + */ + getPath() { + // Default to website URL + return window.location.href; + } + + /** + * @returns {HTMLElement} The current active page element where sticky notes can be applied + */ + getActivePage() { + // Default to root element + return document.documentElement; + } + + /** + * Checks if a path is applicable given the context + * @param {string} path Can be a site URL or another context-specific path + * @returns {boolean} Whether the path matches the current context state + */ + isPathApplicable(path) { + // Default to website URL matching + const currentUrl = window.location.href; + const stickyNoteWebsite = path.split("?")[0]; + const currentWebsite = currentUrl.split("?")[0]; + + if (stickyNoteWebsite !== currentWebsite) { + return false; + } + + const pathParams = parseUrlParams(path); + const currentParams = parseUrlParams(currentUrl); + + if (window.location.hostname === "www.youtube.com") { + if (currentParams.v !== undefined && currentParams.v !== pathParams.v) { + return false; + } + } + return true; + } + + areStickyNotesEnabled() { + return true; + } + } + + class LocalContext extends Context { + + /** + * @override + * @returns {boolean} + */ + isContextActive() { + return window.location.hostname === "127.0.0.1" + || window.location.hostname === "localhost" + || window.location.hostname.startsWith("192.168."); + } + + /** + * @override + * @returns {Promise} + */ + async getSaveData() { + log("Loading save data from localStorage"); + return JSON.parse(localStorage.getItem(SAVE_KEY) ?? "{}"); + } + + /** + * @override + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + log("Saving data to localStorage"); + localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); + } + + /** @override */ + resetSaveData() { + log("Resetting save data in localStorage"); + localStorage.removeItem(SAVE_KEY); + } + } + + class UserScriptContext extends Context { + + /** + * @override + * @returns {boolean} + */ + isContextActive() { + // @ts-expect-error + return typeof GM_getValue === "function"; + } + + /** + * @override + * @returns {Promise} + */ + async getSaveData() { + log("Loading save data from UserScript storage"); + /** @type {BirbSaveData|{}} */ + let saveData = {}; + // @ts-expect-error + saveData = GM_getValue(SAVE_KEY, {}) ?? {}; + return saveData; + } + + /** + * @override + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + log("Saving data to UserScript storage"); + // @ts-expect-error + GM_setValue(SAVE_KEY, saveData); + } + + /** @override */ + resetSaveData() { + log("Resetting save data in UserScript storage"); + // @ts-expect-error + GM_deleteValue(SAVE_KEY); + } + } + + class BrowserExtensionContext extends Context { + + /** + * @override + * @returns {boolean} + */ + isContextActive() { + // @ts-expect-error + return typeof chrome !== "undefined" && typeof chrome.storage !== "undefined" && typeof chrome.storage.sync !== "undefined"; + } + + /** + * @override + * @returns {Promise} + */ + async getSaveData() { + log("Loading save data from browser extension storage"); + return new Promise((resolve) => { + // @ts-expect-error + chrome.storage.sync.get([SAVE_KEY], (result) => { + resolve(result[SAVE_KEY] ?? {}); + }); + }); + } + + /** + * @override + * @param {BirbSaveData} saveData + */ + async putSaveData(saveData) { + log("Saving data to browser extension storage"); + // @ts-expect-error + chrome.storage.sync.set({ [SAVE_KEY]: saveData }, function () { + // @ts-expect-error + if (chrome.runtime.lastError) { + // @ts-expect-error + console.error(chrome.runtime.lastError); + } else { + console.log("Settings saved successfully"); + } + }); + } + + /** @override */ + resetSaveData() { + log("Resetting save data in browser extension storage"); + // @ts-expect-error + chrome.storage.sync.clear(); + } + } + + class ObsidianContext extends Context { + + /** + * @override + * @returns {boolean} + */ + isContextActive() { + // @ts-expect-error + return typeof app !== "undefined" && typeof app.vault !== "undefined"; + } + + /** + * @override + * @returns {Promise} + */ + async getSaveData() { + return new Promise((resolve) => { + // @ts-expect-error + OBSIDIAN_PLUGIN.loadData().then((data) => { + resolve(data ?? {}); + }); + }); + } + + /** + * @override + * @param {BirbSaveData|{}} saveData + */ + async putSaveData(saveData) { + // @ts-expect-error + await OBSIDIAN_PLUGIN.saveData(saveData); + } + + /** @override */ + resetSaveData() { + this.putSaveData({}); + } + + /** @override */ + getFocusableElements() { + const elements = [ + ".workspace-leaf", + ".cm-callout", + ".HyperMD-codeblock-begin", + ".status-bar", + ".mobile-navbar" + ]; + return super.getFocusableElements().concat(elements); + } + + /** @override */ + getPath() { + // @ts-expect-error + const file = app.workspace.getActiveFile(); + if (file && this.getActiveEditorElement()) { + return file.path; + } else { + return ROOT_PATH; + } + } + + /** @override */ + getActivePage() { + if (this.getPath() === ROOT_PATH) { + // Root page, use document element + return document.documentElement + } + return this.getActiveEditorElement() ?? document.documentElement; + } + + /** @override */ + isPathApplicable(path) { + return path === this.getPath(); + } + + /** @override */ + areStickyNotesEnabled() { + return this.getPath() !== ROOT_PATH; + } + + /** @returns {HTMLElement|null} */ + getActiveEditorElement() { + // @ts-expect-error + const activeLeaf = app.workspace.activeLeaf; + const leafElement = activeLeaf?.view?.containerEl; + return leafElement?.querySelector(".cm-scroller") ?? null; + } + } + + const contextProcessingOrder = [ + new UserScriptContext(), + new ObsidianContext(), + new BrowserExtensionContext(), + new LocalContext() + ]; + + const CONTEXTS_BY_KEY = { + "local": LocalContext, + "userscript": UserScriptContext, + "browser-extension": BrowserExtensionContext, + "obsidian": ObsidianContext + }; + + /** + * Determines and returns the current context + * @returns {Context} + */ + function getContext() { + if (CONTEXTS_BY_KEY[SET_CONTEXT]) { + return new CONTEXTS_BY_KEY[SET_CONTEXT](); + } + for (const context of contextProcessingOrder) { + if (context.isContextActive()) { + return context; + } + } + error("No applicable context found, defaulting to LocalContext"); + return new LocalContext(); + } + + /** + * Parse URL parameters into a key-value map + * @param {string} url + * @returns {Record} + */ + function parseUrlParams(url) { + const queryString = url.split("?")[1]; + if (!queryString) return {}; + + return queryString.split("&").reduce((params, param) => { + const [key, value] = param.split("="); + return { ...params, [key]: value }; + }, {}); + } + + /** + * @typedef {Object} SavedStickyNote + * @property {string} id + * @property {string} site + * @property {string} content + * @property {number} top + * @property {number} left + */ + + class StickyNote { + /** + * @param {string} id + * @param {string} [site] + * @param {string} [content] + * @param {number} [top] + * @param {number} [left] + */ + constructor(id, site = "", content = "", top = 0, left = 0) { + this.id = id; + this.site = site; + this.content = content; + this.top = top; + this.left = left; + } + } + + /** + * @param {StickyNote} stickyNote + * @param {HTMLElement} page + * @param {() => void} onSave + * @param {() => void} onDelete + * @returns {HTMLElement} + */ + function renderStickyNote(stickyNote, page, onSave, onDelete) { + const noteElement = makeElement("birb-window"); + noteElement.classList.add("birb-sticky-note"); + const color = getColor(stickyNote.id); + noteElement.style.setProperty("--birb-highlight", color); + noteElement.style.setProperty("--birb-border-color", color); + + // Create header + const header = makeElement("birb-window-header"); + const titleDiv = makeElement("birb-window-title", "Sticky Note"); + const closeButton = makeElement("birb-window-close", "x"); + header.appendChild(titleDiv); + header.appendChild(closeButton); + + // Create content + const content = makeElement("birb-window-content"); + const textarea = document.createElement("textarea"); + textarea.className = "birb-sticky-note-input"; + textarea.style.width = "150px"; + textarea.placeholder = "Write your notes here and they'll stick to the page!"; + textarea.value = stickyNote.content; + content.appendChild(textarea); + + noteElement.appendChild(header); + noteElement.appendChild(content); + + noteElement.style.top = `${stickyNote.top}px`; + noteElement.style.left = `${stickyNote.left}px`; + page.appendChild(noteElement); + + makeDraggable(header, true, (top, left) => { + stickyNote.top = top; + stickyNote.left = left; + onSave(); + }, page); + + if (closeButton) { + makeClosable(() => { + if (stickyNote.content.trim() === "" || confirm("Are you sure you want to delete this sticky note?")) { + onDelete(); + noteElement.remove(); + } + }, closeButton, false); + } + + if (textarea && textarea instanceof HTMLTextAreaElement) { + let saveTimeout; + // Save after debounce + textarea.addEventListener("input", () => { + stickyNote.content = textarea.value; + if (saveTimeout) { + clearTimeout(saveTimeout); + } + saveTimeout = setTimeout(() => { + onSave(); + }, 250); + }); + } + + // On window resize + window.addEventListener("resize", () => { + const modTop = `${stickyNote.top - Math.min(window.innerHeight - noteElement.offsetHeight, stickyNote.top)}px`; + const modLeft = `${stickyNote.left - Math.min(window.innerWidth - noteElement.offsetWidth, stickyNote.left)}px`; + noteElement.style.transform = `scale(var(--birb-ui-scale)) translate(-${modLeft}, -${modTop})`; + }); + + return noteElement; + } + + /** + * @param {StickyNote[]} stickyNotes + * @param {() => void} onSave + * @param {(note: StickyNote) => void} onDelete + */ + function drawStickyNotes(stickyNotes, onSave, onDelete) { + // Remove all existing sticky notes + const existingNotes = document.querySelectorAll(".birb-sticky-note"); + existingNotes.forEach(note => note.remove()); + // Render all sticky notes + const pageElement = getContext().getActivePage(); + const context = getContext(); + for (let stickyNote of stickyNotes) { + if (context.isPathApplicable(stickyNote.site)) { + renderStickyNote(stickyNote, pageElement, onSave, () => onDelete(stickyNote)); + } + } + } + + /** + * @param {StickyNote[]} stickyNotes + * @param {() => void} onSave + * @param {(note: StickyNote) => void} onDelete + */ + function createNewStickyNote(stickyNotes, onSave, onDelete) { + if (getContext().areStickyNotesEnabled() === false) { + return; + } + const id = Date.now().toString(); + const site = getContext().getPath(); + const stickyNote = new StickyNote(id, site, ""); + const page = getContext().getActivePage(); + const element = renderStickyNote(stickyNote, page, onSave, () => onDelete(stickyNote)); + element.style.left = `${page.clientWidth / 2 - element.offsetWidth / 2}px`; + element.style.top = `${page.scrollTop + page.clientHeight / 2 - element.offsetHeight / 2}px`; + stickyNote.top = parseInt(element.style.top, 10); + stickyNote.left = parseInt(element.style.left, 10); + stickyNotes.push(stickyNote); + onSave(); + } + + /** + * Get a color based on the mod of the sticky note ID + * @param {string} id + * @returns {string} A color hex code + */ + function getColor(id) { + const colors = ["#ff8baa", "#79bcff", "#d18bff", "#6de192", "#ffd17c", "#ffb37c", "#ff7c7c"]; + const index = parseInt(id, 10) % colors.length; + return colors[index]; + } + + const MENU_ID = "birb-menu"; + const MENU_EXIT_ID = "birb-menu-exit"; + + class MenuItem { + /** + * @param {string} text + * @param {() => void} action + * @param {boolean} [removeMenu] + */ + constructor(text, action, removeMenu = true) { + this.text = text; + this.action = action; + this.removeMenu = removeMenu; + } + } + + class ConditionalMenuItem extends MenuItem { + /** + * @param {string} text + * @param {() => void} action + * @param {() => boolean} condition + * @param {boolean} [removeMenu] + */ + constructor(text, action, condition, removeMenu = true) { + super(text, action, removeMenu); + this.condition = condition; + } + } + + class DebugMenuItem extends ConditionalMenuItem { + /** + * @param {string} text + * @param {() => void} action + */ + constructor(text, action, removeMenu = true) { + super(text, action, () => isDebug(), removeMenu); + } + } + + class Separator extends MenuItem { + constructor() { + super("", () => { }); + } + } + + /** + * @param {MenuItem} item + * @param {() => void} removeMenuCallback + * @returns {HTMLElement} + */ + function makeMenuItem(item, removeMenuCallback) { + if (item instanceof Separator) { + return makeElement("birb-window-separator"); + } + let menuItem = makeElement("birb-menu-item", item.text); + onClick(menuItem, () => { + if (item.removeMenu) { + removeMenuCallback(); + } + item.action(); + }); + return menuItem; + } + + /** + * Add the menu to the page if it doesn't already exist + * @param {MenuItem[]} menuItems + * @param {string} title + * @param {(menu: HTMLElement) => void} updateLocationCallback + */ + function insertMenu(menuItems, title, updateLocationCallback) { + if (document.querySelector("#" + MENU_ID)) { + return; + } + let menu = makeElement("birb-window", undefined, MENU_ID); + let header = makeElement("birb-window-header"); + const titleDiv = makeElement("birb-window-title", title); + header.appendChild(titleDiv); + let content = makeElement("birb-window-content"); + const removeCallback = () => removeMenu(); + for (const item of menuItems) { + if (!(item instanceof ConditionalMenuItem) || item.condition()) { + content.appendChild(makeMenuItem(item, removeCallback)); + } + } + menu.appendChild(header); + menu.appendChild(content); + document.body.appendChild(menu); + makeDraggable(document.querySelector(".birb-window-header")); + + let menuExit = makeElement("birb-window-exit", undefined, MENU_EXIT_ID); + onClick(menuExit, removeCallback); + document.body.appendChild(menuExit); + makeClosable(removeCallback); + + updateLocationCallback(menu); + } + + /** + * Remove the menu from the page + */ + function removeMenu() { + const menu = document.querySelector("#" + MENU_ID); + if (menu) { + menu.remove(); + } + const exitMenu = document.querySelector("#" + MENU_EXIT_ID); + if (exitMenu) { + exitMenu.remove(); + } + } + + /** + * @returns {boolean} Whether the menu element is on the page + */ + function isMenuOpen() { + return document.querySelector("#" + MENU_ID) !== null; + } + + /** + * @param {MenuItem[]} menuItems + * @param {(menu: HTMLElement) => void} updateLocationCallback + */ + function switchMenuItems(menuItems, updateLocationCallback) { + const menu = document.querySelector("#" + MENU_ID); + if (!menu || !(menu instanceof HTMLElement)) { + return; + } + const content = menu.querySelector(".birb-window-content"); + if (!content) { + error("Birb: Content not found"); + return; + } + while (content.firstChild) { + content.removeChild(content.firstChild); + } + const removeCallback = () => removeMenu(); + for (const item of menuItems) { + if (!(item instanceof ConditionalMenuItem) || item.condition()) { + content.appendChild(makeMenuItem(item, removeCallback)); + } + } + updateLocationCallback(menu); + } + + /** + * @typedef {import('./stickyNotes.js').SavedStickyNote} SavedStickyNote + */ + + /** + * @typedef {Object} BirbSaveData + * @property {string[]} unlockedSpecies + * @property {string} currentSpecies + * @property {Partial} settings + * @property {SavedStickyNote[]} [stickyNotes] + */ + + /** + * @typedef {typeof DEFAULT_SETTINGS} Settings + */ + const DEFAULT_SETTINGS = { + birbMode: false + }; + + // Rendering constants + const SPRITE_WIDTH = 32; + const SPRITE_HEIGHT = 32; + const FEATHER_SPRITE_WIDTH = 32; + const BIRB_CSS_SCALE = 1; + const UI_CSS_SCALE = isMobile() ? 0.9 : 1; + const CANVAS_PIXEL_SIZE = 1; + const WINDOW_PIXEL_SIZE = CANVAS_PIXEL_SIZE * BIRB_CSS_SCALE; + + // Build-time assets + const STYLESHEET = `@font-face { + font-family: 'Monocraft'; + src: url("https://cdn.jsdelivr.net/gh/idreesinc/Monocraft@99b32ab40612ff2533a69d8f14bd8b3d9e604456/dist/Monocraft.otf") format('opentype'); + font-weight: normal; + font-style: normal; +} + +:root { + --birb-border-size: 2px; + --birb-neg-border-size: calc(var(--birb-border-size) * -1); + --birb-double-border-size: calc(var(--birb-border-size) * 2); + --birb-neg-double-border-size: calc(var(--birb-neg-border-size) * 2); + --birb-highlight: #ffa3cb; + --birb-border-color: var(--birb-highlight); + --birb-background-color: #ffecda; + --birb-mix-color: color-mix(in srgb, var(--birb-highlight) 50%, var(--birb-background-color)); + --birb-scale: ${BIRB_CSS_SCALE}; + --birb-ui-scale: ${UI_CSS_SCALE}; +} + +#birb { + image-rendering: pixelated; + position: fixed; + bottom: 0; + transform: scale(var(--birb-scale)) !important; + transform-origin: bottom; + z-index: 2147483638 !important; + cursor: pointer; +} + +.birb-absolute { + position: absolute !important; +} + +.birb-decoration { + image-rendering: pixelated; + position: fixed; + bottom: 0; + transform: scale(var(--birb-scale)) !important; + transform-origin: bottom; + z-index: 2147483630 !important; +} + +.birb-window { + font-family: "Monocraft", monospace !important; + line-height: initial !important; + color: #000000 !important; + z-index: 2147483639 !important; + position: fixed; + background-color: var(--birb-background-color); + box-shadow: + var(--birb-border-size) 0 var(--birb-border-color), + var(--birb-neg-border-size) 0 var(--birb-border-color), + 0 var(--birb-neg-border-size) var(--birb-border-color), + 0 var(--birb-border-size) var(--birb-border-color), + var(--birb-double-border-size) 0 var(--birb-border-color), + var(--birb-neg-double-border-size) 0 var(--birb-border-color), + 0 var(--birb-neg-double-border-size) var(--birb-border-color), + 0 var(--birb-double-border-size) var(--birb-border-color), + 0 0 0 var(--birb-border-size) var(--birb-border-color), + 0 0 0 var(--birb-double-border-size) white, + var(--birb-double-border-size) 0 0 var(--birb-border-size) white, + var(--birb-neg-double-border-size) 0 0 var(--birb-border-size) white, + 0 var(--birb-neg-double-border-size) 0 var(--birb-border-size) white, + 0 var(--birb-double-border-size) 0 var(--birb-border-size) white; + box-sizing: border-box; + display: flex; + flex-direction: column; + transform: scale(var(--birb-ui-scale)) !important; + animation: pop-in 0.08s; + transition-timing-function: ease-in; +} + +#birb-menu { + transition-duration: 0.2s; + transition-timing-function: ease-out; + min-width: 140px; + z-index: 2147483639 !important; +} + +#birb-menu-exit { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 2147483637 !important; +} + +@keyframes pop-in { + 0% { + opacity: 1; + transform: scale(0.1); + } + + 100% { + opacity: 1; + transform: scale(var(--birb-ui-scale)); + } +} + +.birb-window-header { + box-sizing: border-box; + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + padding: 7px; + padding-top: 3px; + padding-bottom: 3px; + padding-left: 30px; + padding-right: 30px; + background-color: var(--birb-highlight); + box-shadow: + var(--birb-border-size) 0 var(--birb-highlight), + var(--birb-neg-border-size) 0 var(--birb-highlight), + 0 var(--birb-neg-border-size) var(--birb-highlight), + var(--birb-neg-border-size) var(--birb-border-size) var(--birb-border-color), + var(--birb-border-size) var(--birb-border-size) var(--birb-border-color); + color: var(--birb-border-color) !important; + font-size: 16px; +} + +.birb-window-title { + text-align: center; + flex-grow: 1; + user-select: none; + color: var(--birb-background-color); + white-space: nowrap; +} + +.birb-window-close { + position: absolute; + top: 1px; + right: 0; + color: var(--birb-background-color); + user-select: none; + cursor: pointer; + padding-left: 5px; + padding-right: 5px; +} + +.birb-window-close:hover { + transform: scale(1.1); +} + +.birb-window-content { + box-sizing: border-box; + background-color: var(--birb-background-color); + margin-top: var(--birb-border-size); + flex-grow: 1; + box-shadow: + var(--birb-border-size) 0 var(--birb-background-color), + var(--birb-neg-border-size) 0 var(--birb-background-color), + 0 var(--birb-border-size) var(--birb-background-color), + 0 var(--birb-neg-border-size) var(--birb-border-color), + 0 var(--birb-border-size) var(--birb-border-color); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding-top: calc(var(--birb-double-border-size)); + padding-bottom: var(--birb-border-size); +} + +.birb-pico-8-content { + background: #111111; + box-shadow: none; + display: flex; + justify-content: center; + overflow: hidden; + border: none; +} + +.birb-pico-8-content iframe { + width: 300px; + margin-left: -15px; + margin-right: -30px; + margin-top: -10px; + margin-bottom: -23px; + border: none; + aspect-ratio: 1; +} + +.birb-music-player-content { + background: var(--birb-background-color); + box-shadow: + var(--birb-border-size) 0 var(--birb-background-color), + var(--birb-neg-border-size) 0 var(--birb-background-color), + 0 var(--birb-border-size) var(--birb-background-color), + 0 var(--birb-neg-border-size) var(--birb-border-color), + 0 var(--birb-border-size) var(--birb-border-color); + display: flex; + justify-content: center; + overflow: hidden; + padding: 10px; +} + +.birb-menu-item { + width: calc(100% - var(--birb-double-border-size)); + font-size: 14px; + padding-top: 4px; + padding-bottom: 4px; + padding-left: 10px; + padding-right: 10px; + box-sizing: border-box; + opacity: 0.7 !important; + user-select: none; + display: flex; + justify-content: space-between; + cursor: pointer; + color: black !important; +} + +.birb-menu-item:hover { + opacity: 1 !important; + background: var(--birb-highlight) !important; + color: white !important; + box-shadow: + var(--birb-border-size) 0 var(--birb-highlight), + var(--birb-neg-border-size) 0 var(--birb-highlight), + 0 var(--birb-neg-border-size) var(--birb-highlight), + 0 var(--birb-border-size) var(--birb-highlight); +} + +.birb-menu-item-arrow { + display: inline-block; +} + +.birb-window-separator { + width: 100%; + height: var(--birb-border-size); + background-color: var(--birb-border-color); + box-sizing: border-box; + margin-top: var(--birb-double-border-size); + margin-bottom: var(--birb-double-border-size); + opacity: 0.4; +} + +#birb-field-guide { + width: 322px !important; +} + +.birb-grid-content { + display: grid; + grid-template-rows: repeat(3, auto); + grid-auto-flow: column; + gap: 10px; + padding-top: 8px; + padding-bottom: 8px; + padding-left: 10px; + padding-right: 10px; + box-sizing: border-box; + justify-content: center; + align-items: center; +} + +.birb-grid-item { + width: 64px; + height: 64px; + overflow: hidden; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; +} + +.birb-grid-item:hover { + border-color: var(--birb-highlight); +} + +.birb-grid-item canvas { + image-rendering: pixelated; + transform: scale(2) !important; + padding-bottom: var(--birb-border-size); +} + +.birb-grid-item, .birb-field-guide-description, .birb-message-content { + border: var(--birb-border-size) solid rgb(255, 207, 144); + box-shadow: 0 0 0 var(--birb-border-size) white; + background: rgba(255, 221, 177, 0.5); +} + +.birb-grid-item-locked { + cursor: auto; + filter: grayscale(100%) sepia(30%); +} + +.birb-grid-item-locked canvas { + filter: contrast(90%); +} + +.birb-grid-item-selected { + border: var(--birb-border-size) solid var(--birb-highlight); + background: var(--birb-mix-color); +} + +.birb-field-guide-description { + max-width: calc(100% - 20px); + margin-left: 10px; + margin-right: 10px; + margin-top: 5px; + padding: 8px; + padding-top: 4px; + padding-bottom: 4px; + margin-bottom: 10px; + font-size: 14px; + box-sizing: border-box; + color: rgb(124, 108, 75); +} + +#birb-feather { + cursor: pointer; +} + +.birb-message-content { + box-sizing: border-box; + margin: 2px; + width: 100%; + padding: 10px; + font-size: 14px; + color: rgb(124, 108, 75); +} + +.birb-sticky-note { + position: absolute; + box-sizing: border-box; + animation: fade-in 0.15s ease-in; +} + +@keyframes fade-in { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +.birb-sticky-note > .birb-window-content { + padding: 0; +} + +.birb-sticky-note-input { + width: 100%; + height: 100%; + padding: 10px !important; + resize: both !important; + min-width: 175px !important; + min-height: 135px !important; + box-sizing: border-box !important; + font-family: "Monocraft", monospace !important; + font-size: 14px !important; + color: black !important; + background-color: transparent !important; + border: none !important; +} + +.birb-sticky-note-input:focus { + outline: none !important; + box-shadow: none !important; +}`; + const SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAAgCAYAAABjE6FEAAAAAXNSR0IArs4c6QAABD5JREFUeJztnTFrFEEYht9JLAJidwju2YpdBAvzAyIWaXJXpRS0MBCwEBTJDwghhaAgGLTSyupMY2UqG9PYWQRb7yJyYJEIacxnkZ11bm5n9+7Y3Zm9ex8Imezd7Te7O9+zM7N7G4AQQgghhBBCCJkJlO8KkPAREXG9ppRiGyK1hY23BvgUkI7dbjYBAJ1ud6BcRR0IITOKxLSiSFpRNFTOkmNR8VtRJF8WF0U2NobKZccnpEzmfFeA5NNuNvG00UCn3R4qV8nB58942mgkZULqDgVYI3wJqNPtYrvfH1i23e8nQ2BCCCkFcwj8ZXEx+alqCJxWhypjE0ICQFKoOrZPAZl1oPwImTFE5Hzy3/hddXzfAvIhf0LK5ILvCtSNgxs3vMRVSikREZ+3nvB2F0JmFN3z0b0/9oKqx9cUBJleeEYfAzPp2BuqFr3v9W4XkcqPgS1dtoEZIe0CAM/AxAOy220JAG/zn3HsoNs/83R0cu8DNM+85g9yvqJVJBQwAYDdbksXvcx/KqWSOoTW+7Pzwkee1pHMiyDmzjQaH/QyETHfU0qDsIc+xnKIiITWEEl5PGh+8HqsfQp4FMxUWNvpJcvoPzdOAZriOVy7DzwCdm6/SV7f7bYH5mPKkFEIAiZE41vAGYhSKpHetHNlXsnRXynkWDhXIiIydzEaWHbveQ8f1+ew8uoMAHDy+wgA8P5JNHCWKUJGQwLGoIBvrbTxoPlBv7ewuITUDHGJ7/uPY3x9cd3LBaOyuDKvZOXVGT6uz6EICWYKELGA7r9O70JrASKWIAwZpQYb4yD4FjAJm7Wdnrx/Es36cc6VX6jD9VBwDoH1jbeu1035wZpzSGOSYfLZn96QgLX87Nj2cNy1TaPGJuFwurcsC6v7SpcBYGHVr/x8C3htp+d1Ys8VP+4I1SbPMisaCwune8vY+PUJAPDy8m0AwN3DdyMF+P7jGAAm6orr+Gk9UFvAGt0TTVkXQAnWlv/i26/8+KULuPp6mLgEZOZbySJy9j7rJMGRBWizsLqPmw8Pce3qpdTPWgdiIgH5FjAhmlDEpzndWxYzB+x8q0BA4sr/mRAgDAmmYYsPE/S+fAuYkJDpby3JxoUOMDjyqap9OwWIGkkwV4CI5/VsCZ18OwEANDYPXJ/9H2RC6fgWMCGh099aShr4nZ9vgfO2712C5oXJkPMut2JpEtLyS6OxeVDYhvsWMCEkF9GdEFuEWoIh599Ij8OKNwL9raXM9xUpP2RciTYFbNep6DoQQjJRX19cP084hwhDJleAWkJ5EixTPDo2UoRXVR0IIU4UzofeAyKcKsynYXSePU6eiqHLZT6gwPqid2r8sutACMnHfmJO6Pk41n+FU0qh8+xx8rdZRom9Lr3erPjs+RESBvGXEYAa5ONYj8Q3h6J2uQry4oe+swmZduqWg2Pfl+dcUQUb7js+IWS6+Ac8zd6eLzTjoQAAAABJRU5ErkJggg=="; + const FEATHER_SPRITE_SHEET = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAARhJREFUWIXtlbENwjAQRf8hSiZIRQ+9WQNRUFIAKzACBSsAA1Ag1mAABqCCBomG3hQQ9OMEx4ZDNH5SikSJ3/fZ5wCJRCKRSPwZ0RzMWmtLAhGvQyUAi9mXP/aFaGjJRQQiguHihMvcFMJUVUYlAMuHixPGy4en1WmVQqgHYHkuZjiEj6a2/LjtYzTY0eiZbgC37Mxh1UN3sn/dr6cCz/LHB/DJj9s+2oMdbtdz6TtfFwQHcMvOInfmQNjsgchNWLXmdfK6gyioAu/6uKrsm1kWLAciKuCuey5nYuXAh234bdmZ6INIUw4E/Ix49xtjCmXfzLL8nY/ktdgnAKwxxgIoXIyqmAOwvIqfiN0ALNd21HYBO9XXGMAdnZTYyHWzWjQAAAAASUVORK5CYII="; + + // Element IDs + const FIELD_GUIDE_ID = "birb-field-guide"; + const FEATHER_ID = "birb-feather"; + + const DEFAULT_BIRD = "bluebird"; + + // Birb movement + const HOP_SPEED = 0.07; + const FLY_SPEED = isMobile() ? 0.175 : 0.25; + const HOP_DISTANCE = 35; + + // Timing constants (in milliseconds) + const UPDATE_INTERVAL = 1000 / 60; // 60 FPS + const AFK_TIME = isDebug() ? 0 : 1000 * 5; + const PET_BOOST_DURATION = 1000 * 60 * 5; + const PET_MENU_COOLDOWN = 1000; + const URL_CHECK_INTERVAL = 150; + const HOP_DELAY = 500; + + // Random event chances per tick + const HOP_CHANCE = 1 / (60 * 2.5); // Every 2.5 seconds + const FOCUS_SWITCH_CHANCE = 1 / (60 * 20); // Every 20 seconds + const FEATHER_CHANCE = 1 / (60 * 60 * 60 * 2); // Every 2 hours + + // Feathers + const FEATHER_FALL_SPEED = 1; + const PET_FEATHER_BOOST = 2; + + // Focus element constraints + const MIN_FOCUS_ELEMENT_WIDTH = 100; + + /** @type {Partial} */ + let userSettings = {}; + + /** + * Load the sprite sheet and return the pixel-map template + * @param {string} dataUri + * @param {boolean} [templateColors] + * @returns {Promise} + */ + function loadSpriteSheetPixels(dataUri, templateColors = true) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.src = dataUri; + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext('2d'); + if (!ctx) { + reject(new Error('Failed to get canvas context')); + return; + } + ctx.drawImage(img, 0, 0); + const imageData = ctx.getImageData(0, 0, img.width, img.height); + const pixels = imageData.data; + const hexArray = []; + for (let y = 0; y < img.height; y++) { + const row = []; + for (let x = 0; x < img.width; x++) { + const index = (y * img.width + x) * 4; + const r = pixels[index]; + const g = pixels[index + 1]; + const b = pixels[index + 2]; + const a = pixels[index + 3]; + if (a === 0) { + row.push(Sprite.TRANSPARENT); + continue; + } + const hex = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; + if (!templateColors) { + row.push(hex); + continue; + } + if (SPRITE_SHEET_COLOR_MAP[hex] === undefined) { + error(`Unknown color: ${hex}`); + row.push(Sprite.TRANSPARENT); + } + row.push(SPRITE_SHEET_COLOR_MAP[hex]); + } + hexArray.push(row); + } + resolve(hexArray); + }; + img.onerror = (err) => { + reject(err); + }; + }); + } + + log("Loading sprite sheets..."); + + Promise.all([ + loadSpriteSheetPixels(SPRITE_SHEET), + loadSpriteSheetPixels(FEATHER_SPRITE_SHEET) + ]).then(([birbPixels, featherPixels]) => { + + const SPRITE_SHEET = birbPixels; + const FEATHER_SPRITE_SHEET = featherPixels; + + const featherLayers = { + feather: new Layer(getLayer(FEATHER_SPRITE_SHEET, 0, FEATHER_SPRITE_WIDTH)), + }; + + const featherFrames = { + feather: new Frame([featherLayers.feather]), + }; + + const FEATHER_ANIMATIONS = { + feather: new Anim([ + featherFrames.feather, + ], [ + 1000, + ]), + }; + + const menuItems = [ + new MenuItem(`Pet ${birdBirb()}`, pet), + new MenuItem("Field Guide", insertFieldGuide), + new ConditionalMenuItem("Sticky Note", () => createNewStickyNote(stickyNotes, save, deleteStickyNote), () => getContext().areStickyNotesEnabled()), + new MenuItem(`Hide ${birdBirb()}`, () => birb.setVisible(false)), + new DebugMenuItem("Freeze/Unfreeze", () => { + frozen = !frozen; + }), + new DebugMenuItem("Reset Data", resetSaveData), + new DebugMenuItem("Unlock All", () => { + for (let type in SPECIES) { + unlockBird(type); + } + }), + new DebugMenuItem("Add Feather", () => { + activateFeather(); + }), + new DebugMenuItem("Disable Debug", () => { + setDebug(false); + }), + new Separator(), + new MenuItem("Settings", () => switchMenuItems(settingsItems, updateMenuLocation), false), + ]; + + const settingsItems = [ + new MenuItem("Go Back", () => switchMenuItems(menuItems, updateMenuLocation), false), + new Separator(), + new MenuItem("Toggle Birb Mode", () => { + userSettings.birbMode = !userSettings.birbMode; + save(); + const message = makeElement("birb-message-content"); + message.appendChild(document.createTextNode(`Your ${birdBirb().toLowerCase()} shall now be referred to as "${birdBirb()}"`)); + if (userSettings.birbMode) { + message.appendChild(document.createElement("br")); + message.appendChild(document.createElement("br")); + message.appendChild(document.createTextNode("Welcome back to 2012")); + } + insertModal(`${birdBirb()} Mode`, message); + }), + new Separator(), + new MenuItem("2025.11.15", () => { alert("Thank you for using Pocket Bird! You are on version: 2025.11.15"); }, false), + ]; + + const styleElement = document.createElement("style"); + + /** @type {Birb} */ + let birb; + + const States = { + IDLE: "idle", + HOP: "hop", + FLYING: "flying", + }; + + let frozen = false; + let stateStart = Date.now(); + let currentState = States.IDLE; + let ticks = 0; + // Bird's current position + let birdY = 0; + let birdX = 40; + // Bird's starting position (when flying) + let startX = 0; + let startY = 0; + // Bird's target position (when flying) + let targetX = 0; + let targetY = 0; + /** @type {HTMLElement|null} */ + let focusedElement = null; + let focusedBounds = { left: 0, right: 0, top: 0 }; + let lastActionTimestamp = Date.now(); + /** @type {number[]} */ + let petStack = []; + let currentSpecies = DEFAULT_BIRD; + let unlockedSpecies = [DEFAULT_BIRD]; + // let visible = true; + let lastPetTimestamp = 0; + /** @type {StickyNote[]} */ + let stickyNotes = []; + + async function load() { + /** @type {BirbSaveData|Object} */ + let saveData = await getContext().getSaveData(); + + debug("Loaded data: " + JSON.stringify(saveData)); + + if (!('settings' in saveData)) { + log("No user settings found in save data, starting fresh"); + } + + userSettings = saveData.settings ?? {}; + unlockedSpecies = saveData.unlockedSpecies ?? [DEFAULT_BIRD]; + currentSpecies = saveData.currentSpecies ?? DEFAULT_BIRD; + stickyNotes = []; + + if (saveData.stickyNotes) { + for (let note of saveData.stickyNotes) { + if (note.id) { + stickyNotes.push(new StickyNote(note.id, note.site, note.content, note.top, note.left)); + } + } + } + + log(stickyNotes.length + " sticky notes loaded"); + switchSpecies(currentSpecies); + } + + function save() { + /** @type {BirbSaveData} */ + const saveData = { + unlockedSpecies, + currentSpecies, + settings: userSettings + }; + + if (stickyNotes.length > 0) { + saveData.stickyNotes = stickyNotes.map(note => ({ + id: note.id, + site: note.site, + content: note.content, + top: note.top, + left: note.left + })); + } + + getContext().putSaveData(saveData); + } + + function resetSaveData() { + getContext().resetSaveData(); + load(); + } + + /** + * Get the user settings merged with default settings + * @returns {Settings} The merged settings + */ + function settings() { + return { ...DEFAULT_SETTINGS, ...userSettings }; + } + + /** + * Bird or birb, you decide + */ + function birdBirb() { + return settings().birbMode ? "Birb" : "Bird"; + } + + function init() { + log("Sprite sheets loaded successfully, initializing bird..."); + + if (window !== window.top) { + // Skip installation if within an iframe + log("In iframe, skipping Birb script initialization"); + return; + } + + load().then(onLoad); + } + + function onLoad() { + styleElement.textContent = STYLESHEET; + document.head.appendChild(styleElement); + + birb = new Birb(BIRB_CSS_SCALE, CANVAS_PIXEL_SIZE, SPRITE_SHEET, SPRITE_WIDTH, SPRITE_HEIGHT); + birb.setAnimation(Animations.BOB); + + window.addEventListener("scroll", () => { + lastActionTimestamp = Date.now(); + }); + + onClick(document, (e) => { + lastActionTimestamp = Date.now(); + if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) { + removeMenu(); + } + }); + + const birbElement = birb.getElement(); + + onClick(birbElement, () => { + if (birb.getCurrentAnimation() === Animations.HEART && (Date.now() - lastPetTimestamp < PET_MENU_COOLDOWN)) { + // Currently being pet, don't open menu + return; + } + insertMenu(menuItems, `${birdBirb().toLowerCase()}OS`, updateMenuLocation); + }); + + birbElement.addEventListener("mouseover", () => { + lastActionTimestamp = Date.now(); + if (currentState === States.IDLE) { + petStack.push(Date.now()); + if (petStack.length > 10) { + petStack.shift(); + } + const pets = petStack.filter((time) => Date.now() - time < 1000).length; + if (pets >= 3) { + pet(); + // Clear the stack + petStack = []; + } + } + }); + + birbElement.addEventListener("touchmove", (e) => { + pet(); + }); + + drawStickyNotes(stickyNotes, save, deleteStickyNote); + + let lastPath = getContext().getPath().split("?")[0]; + setInterval(() => { + const currentPath = getContext().getPath().split("?")[0]; + if (currentPath !== lastPath) { + log("Path changed, updating sticky notes: " + currentPath); + lastPath = currentPath; + drawStickyNotes(stickyNotes, save, deleteStickyNote); + } + }, URL_CHECK_INTERVAL); + + setInterval(update, UPDATE_INTERVAL); + + focusOnElement(true); + } + + function update() { + ticks++; + + // Hide bird if the browser is fullscreen + if (document.fullscreenElement) { + birb.setVisible(false); + // Won't be restored on fullscreen exit + } + + if (currentState === States.IDLE && !frozen && !isMenuOpen()) { + if (Date.now() - stateStart > HOP_DELAY && Math.random() < HOP_CHANCE && birb.getCurrentAnimation() !== Animations.HEART) { + hop(); + } else if (Date.now() - lastActionTimestamp > AFK_TIME) { + // Idle for a while, do something + if (focusedElement === null) { + // Fly to an element + focusOnElement(); + lastActionTimestamp = Date.now(); + } else if (Math.random() < FOCUS_SWITCH_CHANCE) { + // Fly to another element if idle for a longer while + focusOnElement(); + lastActionTimestamp = Date.now(); + } + } + } else if (currentState === States.HOP) { + if (updateParabolicPath(HOP_SPEED)) { + setState(States.IDLE); + } + } + + // Double the chance of a feather if recently pet + const petMod = Date.now() - lastPetTimestamp < PET_BOOST_DURATION ? PET_FEATHER_BOOST : 1; + if (birb.isVisible() && Math.random() < FEATHER_CHANCE * petMod) { + lastPetTimestamp = 0; + activateFeather(); + } + updateFeather(); + } + + function draw() { + requestAnimationFrame(draw); + + if (!birb || !birb.isVisible()) { + return; + } + + updateFocusedElementBounds(); + + // Update the bird's position + if (currentState === States.IDLE) { + if (focusedElement && !isWithinHorizontalBounds()) { + flySomewhere(); + } + birdY = getFocusedY(); + } else if (currentState === States.FLYING) { + // Fly to target location (even if in the air) + if (updateParabolicPath(FLY_SPEED, 2)) { + setState(States.IDLE); + } + } + + const oldTargetY = targetY; + targetY = getFocusedY(); + // Adjust startY to account for scrolling + startY += targetY - oldTargetY; + if (targetY < 0 || targetY > getWindowHeight()) { + // Fly to another element or the ground if the focused element moves out of bounds + flySomewhere(); + } + + if (birb.draw(SPECIES[currentSpecies])) { + birb.setAnimation(Animations.STILL); + } + + // Clamp startY, birdY, targetY to a bit above the top of the window + const maxY = getWindowHeight() * 1.5; + startY = Math.min(startY, maxY); + birdY = Math.min(birdY, maxY); + targetY = Math.min(targetY, maxY); + + // Update HTML element position + birb.setX(birdX); + birb.setY(birdY); + } + + /** + * @param {StickyNote} stickyNote + */ + function deleteStickyNote(stickyNote) { + stickyNotes = stickyNotes.filter(note => note.id !== stickyNote.id); + save(); + } + + /** + * Create a window element with header and content + * @param {string} id + * @param {string} title + * @param {HTMLElement} contentElement + * @param {() => void} [onClose] + * @returns {HTMLElement} + */ + function createWindow(id, title, contentElement, onClose) { + const window = makeElement("birb-window", undefined, id); + + const header = makeElement("birb-window-header"); + const titleElement = makeElement("birb-window-title"); + titleElement.textContent = title; + const closeButton = makeElement("birb-window-close"); + closeButton.textContent = "x"; + + header.appendChild(titleElement); + header.appendChild(closeButton); + + const contentWrapper = makeElement("birb-window-content"); + contentWrapper.appendChild(contentElement); + + window.appendChild(header); + window.appendChild(contentWrapper); + + document.body.appendChild(window); + makeDraggable(header); + + makeClosable(() => { + window.remove(); + }, closeButton); + + return window; + } + + function activateFeather() { + if (document.querySelector("#" + FEATHER_ID)) { + return; + } + const speciesToUnlock = Object.keys(SPECIES).filter((species) => !unlockedSpecies.includes(species)); + if (speciesToUnlock.length === 0) { + // No more species to unlock + return; + } + const birdType = speciesToUnlock[Math.floor(Math.random() * speciesToUnlock.length)]; + insertFeather(birdType); + } + + /** + * @param {string} birdType + */ + function insertFeather(birdType) { + let type = SPECIES[birdType]; + const featherCanvas = document.createElement("canvas"); + featherCanvas.id = FEATHER_ID; + featherCanvas.classList.add("birb-decoration"); + featherCanvas.width = FEATHER_SPRITE_WIDTH * CANVAS_PIXEL_SIZE; + featherCanvas.height = FEATHER_SPRITE_WIDTH * CANVAS_PIXEL_SIZE; + const x = featherCanvas.width * 2 + Math.random() * (window.innerWidth - featherCanvas.width * 4); + featherCanvas.style.marginLeft = `${x}px`; + featherCanvas.style.top = `${-featherCanvas.height}px`; + const featherCtx = featherCanvas.getContext("2d"); + if (!featherCtx) { + return; + } + FEATHER_ANIMATIONS.feather.draw(featherCtx, Directions.LEFT, Date.now(), CANVAS_PIXEL_SIZE, type); + document.body.appendChild(featherCanvas); + onClick(featherCanvas, () => { + unlockBird(birdType); + removeFeather(); + if (document.querySelector("#" + FIELD_GUIDE_ID)) { + removeFieldGuide(); + insertFieldGuide(); + } + }); + } + + function removeFeather() { + const feather = document.querySelector("#" + FEATHER_ID); + if (feather) { + feather.remove(); + } + } + + /** + * @param {string} birdType + */ + function unlockBird(birdType) { + if (!unlockedSpecies.includes(birdType)) { + unlockedSpecies.push(birdType); + const message = makeElement("birb-message-content"); + message.appendChild(document.createTextNode("You've found a ")); + const bold = document.createElement("b"); + bold.textContent = SPECIES[birdType].name; + message.appendChild(bold); + message.appendChild(document.createTextNode(" feather! Use the Field Guide to switch your bird's species.")); + insertModal("New Bird Unlocked!", message); + } + save(); + } + + function updateFeather() { + const feather = document.querySelector("#birb-feather"); + if (!feather || !(feather instanceof HTMLElement)) { + return; + } + const y = parseInt(feather.style.top || "0") + FEATHER_FALL_SPEED; + feather.style.top = `${Math.min(y, getWindowHeight() - feather.offsetHeight)}px`; + if (y < getWindowHeight() - feather.offsetHeight) { + feather.style.left = `${Math.sin(3.14 * 2 * (ticks / 120)) * 25}px`; + } + } + + /** + * @param {HTMLElement} element + */ + function centerElement(element) { + element.style.left = `${window.innerWidth / 2 - element.offsetWidth / 2}px`; + element.style.top = `${getWindowHeight() / 2 - element.offsetHeight / 2}px`; + } + + /** + * @param {string} title + * @param {HTMLElement} content + */ + function insertModal(title, content) { + if (document.querySelector("#" + FIELD_GUIDE_ID)) { + return; + } + + const modal = createWindow("birb-modal", title, content); + + modal.style.width = "270px"; + centerElement(modal); + } + + /** + * @param {HTMLElement} menu + */ + function updateMenuLocation(menu) { + let x = birdX; + let y = birb.getElementTop() + birb.getElementHeight() / 2 + WINDOW_PIXEL_SIZE * 10; + const offset = 20; + if (x < window.innerWidth / 2) { + // Left side + x += offset; + } else { + // Right side + x -= (menu.offsetWidth + offset) * UI_CSS_SCALE; + } + if (y > getWindowHeight() / 2) { + // Top side + y -= (menu.offsetHeight + offset + 10) * UI_CSS_SCALE; + } else { + // Bottom side + y += offset; + } + menu.style.left = `${x}px`; + menu.style.top = `${y}px`; + } + function insertFieldGuide() { + if (document.querySelector("#" + FIELD_GUIDE_ID)) { + return; + } + + const contentContainer = document.createElement("div"); + const content = makeElement("birb-grid-content"); + const description = makeElement("birb-field-guide-description"); + contentContainer.appendChild(content); + contentContainer.appendChild(description); + + const fieldGuide = createWindow( + FIELD_GUIDE_ID, + "Field Guide", + contentContainer + ); + + const generateDescription = (/** @type {string} */ speciesId) => { + const type = SPECIES[speciesId]; + const unlocked = unlockedSpecies.includes(speciesId); + + const boldName = document.createElement("b"); + boldName.textContent = type.name; + + const spacer = document.createElement("div"); + spacer.style.height = "0.3em"; + + const descText = document.createTextNode(!unlocked ? "Not yet unlocked" : type.description); + + const fragment = document.createDocumentFragment(); + fragment.appendChild(boldName); + fragment.appendChild(spacer); + fragment.appendChild(descText); + + return fragment; + }; + + description.appendChild(generateDescription(currentSpecies)); + for (const [id, type] of Object.entries(SPECIES)) { + const unlocked = unlockedSpecies.includes(id); + const speciesElement = makeElement("birb-grid-item"); + if (id === currentSpecies) { + speciesElement.classList.add("birb-grid-item-selected"); + } + const speciesCanvas = document.createElement("canvas"); + speciesCanvas.width = SPRITE_WIDTH * CANVAS_PIXEL_SIZE; + speciesCanvas.height = SPRITE_HEIGHT * CANVAS_PIXEL_SIZE; + const speciesCtx = speciesCanvas.getContext("2d"); + if (!speciesCtx) { + return; + } + birb.getFrames().base.draw(speciesCtx, Directions.RIGHT, CANVAS_PIXEL_SIZE, type); + speciesElement.appendChild(speciesCanvas); + content.appendChild(speciesElement); + if (unlocked) { + onClick(speciesElement, () => { + switchSpecies(id); + document.querySelectorAll(".birb-grid-item").forEach((element) => { + element.classList.remove("birb-grid-item-selected"); + }); + speciesElement.classList.add("birb-grid-item-selected"); + }); + } else { + speciesElement.classList.add("birb-grid-item-locked"); + } + speciesElement.addEventListener("mouseover", () => { + description.textContent = ""; + description.appendChild(generateDescription(id)); + }); + speciesElement.addEventListener("mouseout", () => { + description.textContent = ""; + description.appendChild(generateDescription(currentSpecies)); + }); + } + centerElement(fieldGuide); + } + + function removeFieldGuide() { + const fieldGuide = document.querySelector("#" + FIELD_GUIDE_ID); + if (fieldGuide) { + fieldGuide.remove(); + } + } + + /** + * @param {string} type + */ + function switchSpecies(type) { + currentSpecies = type; + // Update CSS variable --birb-highlight to be wing color + document.documentElement.style.setProperty("--birb-highlight", SPECIES[type].colors[Sprite.THEME_HIGHLIGHT]); + save(); + } + + /** + * Update the birds location from the start to the target location on a parabolic path + * @param {number} speed The speed of the bird along the path + * @param {number} [intensity] The intensity of the parabolic path + * @returns {boolean} Whether the bird has reached the target location + */ + function updateParabolicPath(speed, intensity = 2.5) { + const dx = targetX - startX; + const dy = targetY - startY; + const distance = Math.sqrt(dx * dx + dy * dy); + const time = Date.now() - stateStart; + if (distance > Math.max(window.innerWidth, getWindowHeight()) / 2) { + speed *= 1.3; + } + const amount = Math.min(1, time / (distance / speed)); + const { x, y } = parabolicLerp(startX, startY, targetX, targetY, amount, intensity); + birdX = x; + birdY = y; + const complete = Math.abs(birdX - targetX) < 1 && Math.abs(birdY - targetY) < 1; + if (complete) { + birdX = targetX; + birdY = targetY; + } else { + birb.setDirection(targetX > birdX ? Directions.RIGHT : Directions.LEFT); + } + return complete; + } + + function getFocusedElementRandomX() { + return Math.random() * (focusedBounds.right - focusedBounds.left) + focusedBounds.left; + } + + function isWithinHorizontalBounds() { + return birdX >= focusedBounds.left && birdX <= focusedBounds.right; + } + + function getFocusedY() { + return getWindowHeight() - focusedBounds.top; + } + + /** + * Fly to either an element or the ground + */ + function flySomewhere() { + // On mobile, always prefer to focus on an element + // If not mobile, 50% chance to focus on ground + // if ((!isMobile() && coinFlip()) || !focusOnElement()) { + // focusOnGround(); + // } + if (!focusOnElement()) { + focusOnGround(); + } + } + + function focusOnGround() { + focusedElement = null; + updateFocusedElementBounds(); + flyTo(Math.random() * window.innerWidth, 0); + } + + /** + * Focus on an element within the viewport + * @param {boolean} [teleport] Whether to teleport to the element instead of flying + * @returns Whether an element to focus on was found + */ + function focusOnElement(teleport = false) { + if (frozen) { + return false; + } + const MIN_FOCUS_ELEMENT_TOP = getContext().getFocusElementTopMargin(); + const elements = document.querySelectorAll(getContext().getFocusableElements().join(", ")); + const inWindow = Array.from(elements).filter((img) => { + const rect = img.getBoundingClientRect(); + return rect.left >= 0 && rect.top >= MIN_FOCUS_ELEMENT_TOP && rect.right <= window.innerWidth && rect.top <= getWindowHeight(); + }); + const visible = Array.from(inWindow).filter((img) => { + const style = window.getComputedStyle(img); + if (style.display === "none" || style.visibility === "hidden" || (style.opacity && parseFloat(style.opacity) < 0.25)) { + return false; + } + return true; + }); + /** @type {HTMLElement[]} */ + // @ts-expect-error + const largeElements = Array.from(visible).filter((img) => img instanceof HTMLElement && img !== focusedElement && img.offsetWidth >= MIN_FOCUS_ELEMENT_WIDTH); + // Ensure the bird doesn't land on fixed or sticky elements + const fixedAllowed = getContext() instanceof ObsidianContext; + const nonFixedElements = largeElements.filter((el) => { + if (fixedAllowed) { + return true; + } + const style = window.getComputedStyle(el); + return style.position !== "fixed" && style.position !== "sticky"; + }); + if (nonFixedElements.length === 0) { + return false; + } + const randomElement = nonFixedElements[Math.floor(Math.random() * nonFixedElements.length)]; + focusedElement = randomElement; + log("Focusing on element: ", focusedElement); + updateFocusedElementBounds(); + if (teleport) { + teleportTo(getFocusedElementRandomX(), getFocusedY()); + } else { + flyTo(getFocusedElementRandomX(), getFocusedY()); + } + return randomElement !== null; + } + + /** + * @param {number} x + * @param {number} y + */ + function teleportTo(x, y) { + birdX = x; + birdY = y; + setState(States.IDLE); + } + + function updateFocusedElementBounds() { + if (focusedElement === null) { + // Update ground location to bottom of window + focusedBounds = { left: 0, right: window.innerWidth, top: getWindowHeight() }; + return; + } + let { left, right, top } = focusedElement.getBoundingClientRect(); + if (focusedElement.classList.contains("birb-sticky-note")) { + top -= 4.5 * UI_CSS_SCALE; + if (focusedBounds.left !== left) { + // Sticky note has moved + const oldWidth = focusedBounds.right - focusedBounds.left; + const newWidth = right - left; + if (oldWidth === newWidth) { + // Move bird along with note + if (currentState === States.IDLE) { + birdX += left - focusedBounds.left; + } else if (currentState === States.HOP) { + startX += left - focusedBounds.left; + startY += top - focusedBounds.top; + targetX += left - focusedBounds.left; + targetY += top - focusedBounds.top; + } + } + } + } + focusedBounds = { left, right, top }; + } + + function hop() { + if (frozen) { + return; + } + if (currentState === States.IDLE) { + setState(States.HOP); + birb.setAnimation(Animations.FLYING); + if ((Math.random() < 0.5 && birdX - HOP_DISTANCE > focusedBounds.left) || birdX + HOP_DISTANCE > focusedBounds.right) { + targetX = birdX - HOP_DISTANCE; + } else { + targetX = birdX + HOP_DISTANCE; + } + targetY = getFocusedY(); + } + } + + function pet() { + if (currentState === States.IDLE && birb.getCurrentAnimation() !== Animations.HEART) { + birb.setAnimation(Animations.HEART); + lastPetTimestamp = Date.now(); + } + } + + /** + * @param {number} x + * @param {number} y + */ + function flyTo(x, y) { + targetX = x; + targetY = y; + setState(States.FLYING); + birb.setAnimation(Animations.FLYING); + } + + /** + * @returns {boolean} Whether the bird should be absolutely positioned + */ + function isAbsolute() { + return focusedElement !== null && (currentState === States.IDLE || currentState === States.HOP); + } + + /** + * Set the current state and reset the state timer + * @param {string} state + */ + function setState(state) { + stateStart = Date.now(); + startX = birdX; + startY = birdY; + currentState = state; + if (state === States.IDLE) { + birb.setAnimation(Animations.BOB); + } + birb.setAbsolutePositioned(isAbsolute()); + birb.setY(birdY); + } + + // Helper functions + + /** + * @param {number} startX + * @param {number} startY + * @param {number} endX + * @param {number} endY + * @param {number} amount + * @param {number} [intensity] + * @returns {{x: number, y: number}} + */ + function parabolicLerp(startX, startY, endX, endY, amount, intensity = 1.2) { + const dx = endX - startX; + const dy = endY - startY; + const distance = Math.sqrt(dx * dx + dy * dy); + const angle = Math.atan2(dy, dx); + const midX = startX + Math.cos(angle) * distance / 2; + const midY = startY + Math.sin(angle) * distance / 2 + distance / 4 * intensity; + const t = amount; + const x = (1 - t) ** 2 * startX + 2 * (1 - t) * t * midX + t ** 2 * endX; + const y = (1 - t) ** 2 * startY + 2 * (1 - t) * t * midY + t ** 2 * endY; + return { x, y }; + } + + // Run the birb + init(); + draw(); + }).catch((e) => { + error("Error while loading sprite sheets: ", e); + }); + +})(); + +}; \ No newline at end of file diff --git a/platform-specific/vencord/wrapper.js b/platform-specific/vencord/wrapper.js new file mode 100644 index 0000000..9205fc7 --- /dev/null +++ b/platform-specific/vencord/wrapper.js @@ -0,0 +1,3 @@ +export const Birb = () => { +__CODE__ +}; \ No newline at end of file diff --git a/src/context.js b/src/context.js index 5811414..bfce713 100644 --- a/src/context.js +++ b/src/context.js @@ -2,6 +2,7 @@ import { debug, log, error } from "./shared.js"; const SAVE_KEY = "birbSaveData"; const ROOT_PATH = ""; +const SET_CONTEXT = "__CONTEXT__" /** * @typedef {import('./application.js').BirbSaveData} BirbSaveData @@ -188,7 +189,7 @@ class BrowserExtensionContext extends Context { */ isContextActive() { // @ts-expect-error - return typeof chrome !== "undefined"; + return typeof chrome !== "undefined" && typeof chrome.storage !== "undefined" && typeof chrome.storage.sync !== "undefined"; } /** @@ -232,6 +233,7 @@ class BrowserExtensionContext extends Context { } export class ObsidianContext extends Context { + /** * @override * @returns {boolean} @@ -319,15 +321,29 @@ export class ObsidianContext extends Context { } } -const CONTEXTS = [ +const contextProcessingOrder = [ new UserScriptContext(), new ObsidianContext(), new BrowserExtensionContext(), new LocalContext() ]; +const CONTEXTS_BY_KEY = { + "local": LocalContext, + "userscript": UserScriptContext, + "browser-extension": BrowserExtensionContext, + "obsidian": ObsidianContext +}; + +/** + * Determines and returns the current context + * @returns {Context} + */ export function getContext() { - for (const context of CONTEXTS) { + if (CONTEXTS_BY_KEY[SET_CONTEXT]) { + return new CONTEXTS_BY_KEY[SET_CONTEXT](); + } + for (const context of contextProcessingOrder) { if (context.isContextActive()) { return context; }