From 1dac644b0bc1d4929b676771bd56fea891934c10 Mon Sep 17 00:00:00 2001 From: "[Ikelene]" Date: Tue, 2 Dec 2025 18:30:11 -0700 Subject: [PATCH 1/7] update url --- static/extensions/Ikelene/googleAuthExtension.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/extensions/Ikelene/googleAuthExtension.js b/static/extensions/Ikelene/googleAuthExtension.js index e852ab5e..c3dc6978 100644 --- a/static/extensions/Ikelene/googleAuthExtension.js +++ b/static/extensions/Ikelene/googleAuthExtension.js @@ -69,7 +69,7 @@ class GoogleAuthExtension { login() { const clientId = '382430967410-3svk456rj8ntlu3d3gd9oma09i96cpr9.apps.googleusercontent.com'; - const redirectUri = 'https://cadex.dev/google/googleLogin.php'; + const redirectUri = 'https://ikelene.dev/google/googleLogin.php'; const scope = 'profile email'; // 🔥 Get the current domain to pass as "source" @@ -86,7 +86,7 @@ class GoogleAuthExtension { // Listen for messages from the redirect page window.addEventListener('message', (event) => { - if (event.origin === 'https://cadex.dev') { + if (event.origin === 'https://ikelene.dev') { const { accountName, fullName, profilePicture, userId, locale, emailVerified } = event.data; this.accountName = accountName; this.fullName = fullName; From b4b85050d67ca2a4d86e25bc879845e533037042 Mon Sep 17 00:00:00 2001 From: "[Ikelene]" Date: Tue, 2 Dec 2025 18:38:07 -0700 Subject: [PATCH 2/7] remove my ahh comments they suck --- static/extensions/Ikelene/googleAuthExtension.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/static/extensions/Ikelene/googleAuthExtension.js b/static/extensions/Ikelene/googleAuthExtension.js index c3dc6978..7b6dc5fd 100644 --- a/static/extensions/Ikelene/googleAuthExtension.js +++ b/static/extensions/Ikelene/googleAuthExtension.js @@ -72,19 +72,14 @@ class GoogleAuthExtension { const redirectUri = 'https://ikelene.dev/google/googleLogin.php'; const scope = 'profile email'; - // 🔥 Get the current domain to pass as "source" const sourceDomain = window.location.hostname; - // Encode sourceDomain in the "state" parameter const state = encodeURIComponent(JSON.stringify({ source: sourceDomain })); - // Build the OAuth URL const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}&prompt=select_account&state=${state}`; - // Open the login popup this.authWindow = window.open(authUrl, 'Google Login', 'width=500,height=600'); - // Listen for messages from the redirect page window.addEventListener('message', (event) => { if (event.origin === 'https://ikelene.dev') { const { accountName, fullName, profilePicture, userId, locale, emailVerified } = event.data; From 4822cd4406f87484cba4cac0b6707bd334115c53 Mon Sep 17 00:00:00 2001 From: "[Ikelene]" Date: Sat, 27 Dec 2025 09:04:49 -0700 Subject: [PATCH 3/7] Create serverStorageExtension.js --- .../Ikelene/serverStorageExtension.js | 288 ++++++++++++++++++ 1 file changed, 288 insertions(+) create mode 100644 static/extensions/Ikelene/serverStorageExtension.js diff --git a/static/extensions/Ikelene/serverStorageExtension.js b/static/extensions/Ikelene/serverStorageExtension.js new file mode 100644 index 00000000..b2369c12 --- /dev/null +++ b/static/extensions/Ikelene/serverStorageExtension.js @@ -0,0 +1,288 @@ +(function (Scratch) { + 'use strict'; + + class ServerStorage { + constructor() { + this.serverUrl = 'https://ikelene.dev/storage/'; + this.apiKey = null; + this.maxDataSize = 262144; + } + + getInfo() { + return { + id: 'ikeleneServerStorage', + name: 'Server Storage', + color1: '#ff9bfd', + color2: '#ff9bfd', + color3: '#ff9bfd', + docsURI: this.serverUrl + 'apiKey.html', + blocks: [ + { + blockType: Scratch.BlockType.LABEL, + text: 'press "Open Docs" to get API key' + }, + { + opcode: 'setServerUrl', + blockType: Scratch.BlockType.COMMAND, + text: 'set server to [SERVER] server', + arguments: { + SERVER: { + type: Scratch.ArgumentType.STRING, + menu: 'serverMenu', + defaultValue: 'global' + } + } + }, + { + opcode: 'setApiKey', + blockType: Scratch.BlockType.COMMAND, + text: 'set API key to [APIKEY]', + arguments: { + APIKEY: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'paste-your-api-key-here' + } + } + }, + { + opcode: 'saveToServer', + blockType: Scratch.BlockType.COMMAND, + text: 'save [VALUE] to server as [KEY]', + arguments: { + VALUE: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'value' + }, + KEY: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'key' + } + } + }, + { + opcode: 'getFromServer', + blockType: Scratch.BlockType.REPORTER, + text: 'get [KEY] from server', + arguments: { + KEY: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'key' + } + } + }, + { + opcode: 'serverDataExists', + blockType: Scratch.BlockType.BOOLEAN, + text: 'server has [KEY]', + arguments: { + KEY: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'key' + } + } + }, + { + opcode: 'deleteFromServer', + blockType: Scratch.BlockType.COMMAND, + text: 'delete [KEY] from server', + arguments: { + KEY: { + type: Scratch.ArgumentType.STRING, + defaultValue: 'key' + } + } + }, + { + opcode: 'isServerWorking', + blockType: Scratch.BlockType.BOOLEAN, + text: 'is server working?' + } + ], + menus: { + serverMenu: { + acceptReporters: true, + items: ['global'] + } + } + }; + } + + setServerUrl(args) { + const server = args.SERVER; + if (server === 'global') { + this.serverUrl = 'https://ikelene.dev/storage/'; + } else { + const trimmed = server.trim(); + if (trimmed.length === 0) { + return; + } + this.serverUrl = trimmed.endsWith('/') ? trimmed : trimmed + '/'; + } + } + + setApiKey(args) { + this.apiKey = args.APIKEY.trim(); + } + + hasValidKey() { + return this.apiKey && this.apiKey.length > 0; + } + + async saveToServer(args) { + if (!this.hasValidKey()) { + console.warn('You need to set an API key first. You can generate a key if you press the Open Documentation button'); + return; + } + + const value = args.VALUE; + const key = args.KEY; + const size = new TextEncoder().encode(value).length; + + if (size > this.maxDataSize) { + return; + } + + const payload = { + apiKey: this.apiKey, + key: key, + value: value, + mimeType: 'application/json' + }; + + try { + const response = await fetch(this.serverUrl + 'store.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify(payload) + }); + + const result = await response.json(); + if (!result.success) { + console.warn('You need to set an API key first. You can generate a key if you press the Open Documentation button'); + } + } catch (error) { + console.warn('You need to set an API key first. You can generate a key if you press the Open Documentation button'); + } + } + + async getFromServer(args) { + if (!this.hasValidKey()) { + return 'You need to set an API key first. You can generate a key if you press the Open Documentation button'; + } + + const key = args.KEY; + + try { + const response = await fetch(this.serverUrl + 'get.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ + apiKey: this.apiKey, + key: key + }) + }); + + if (response.status === 401) { + return 'You need to set an API key first. You can generate a key if you press the Open Documentation button'; + } + + const result = await response.json(); + if (result.success && result.data) { + return result.data.value || ''; + } + return ''; + } catch (error) { + return 'You need to set an API key first. You can generate a key if you press the Open Documentation button'; + } + } + + async serverDataExists(args) { + if (!this.hasValidKey()) { + return false; + } + + const key = args.KEY; + + try { + const response = await fetch(this.serverUrl + 'get.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ + apiKey: this.apiKey, + key: key + }) + }); + + if (response.status === 401) { + return false; + } + + const result = await response.json(); + return result.success && !!result.data; + } catch (error) { + return false; + } + } + + async deleteFromServer(args) { + if (!this.hasValidKey()) { + console.warn('You need to set an API key first. You can generate a key if you press the Open Documentation button'); + return; + } + + const key = args.KEY; + + try { + const response = await fetch(this.serverUrl + 'delete.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ + apiKey: this.apiKey, + key: key + }) + }); + + const result = await response.json(); + if (!result.success) { + console.warn('Delete failed or key not found'); + } + } catch (error) { + console.warn('Delete failed or server unreachable'); + } + } + + async isServerWorking() { + try { + const response = await fetch(this.serverUrl + 'ping.php', { + method: 'GET', + headers: { + 'Accept': 'application/json' + } + }); + + if (!response.ok) { + return false; + } + + const result = await response.json(); + return result.success === true && result.status === 'ok'; + } catch (error) { + return false; + } + } + } + + Scratch.extensions.register(new ServerStorage()); +})(Scratch); From 746ad189b55893df71c3bf4b798c74e432763365 Mon Sep 17 00:00:00 2001 From: "[Ikelene]" Date: Sat, 27 Dec 2025 09:27:52 -0700 Subject: [PATCH 4/7] avif format for mr. jeremy --- static/images/Ikelene/serverStorageIMG.avif | Bin 0 -> 7415 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 static/images/Ikelene/serverStorageIMG.avif diff --git a/static/images/Ikelene/serverStorageIMG.avif b/static/images/Ikelene/serverStorageIMG.avif new file mode 100644 index 0000000000000000000000000000000000000000..90ad280f0aa9962545d0128a803ac4b1590fc7fd GIT binary patch literal 7415 zcma)BcUY54vwuUECL+BF0i-Apy7aDq^bQIbLV#crLK1ot1px)A7NjX4qI3Zfl&Xj# z(vgk`C{211srL<@b3Ess@4L_4Jb8C^<~OsmGqd~0E&u>LZo~i_+!y5rfQ{5+P;Sx~ z6dW9r$6=5}IEfE70S{L+{uc=VI23~TC;ey7h(i%EhXV-sl|!Mu5Tuc=CO`&|H~@Ls zuO0wc3_%{)*mwTqQ0#(3V-yN|=*FRx6&xTBVW2kL7bZ>85dVV-Awe)G3=)9jaA*_) zPDFWOrSPt9r~EN!5|FG4g~Q{({vHa4@B$h1a9?Sia0+^o>=MEYjmP1=z{$w0&FkNKe$^jt^+UGD?AWcdYJrZke3HDI%&jSzu3*a1} z4e$d7fF7U&C;-xc68KdBBmfye5*z{lMW1Aw2QUG=KwSj*#Q^~T9zX%^fCoSXPJ>(@ z&^i+A-9SDP{GovWQv5+Zu>FoJ!A(}?kOiUF1^~*jgM&3;0H96=fW3r+gPn&52YcCI zfsFt_1NOJR*8>1JvjOrGe)EK~0N`i@06c&Fo9A*D0IH$@fMXB`$HNcjAqT%?ZeV?^ zJ_Y~=djL4z2>`Sge#H!wk>o&5KLA(-fvNfk0IBx@;3SAYdJz<`pa?i52>}B6!H_P3 z1QQPBk2>Vg(2#P?j>2Jn%|(higd&`boC0KXq43CGdjJ3!5w2iqkaiCQ060xBu5cnV zKugPcQu@G)fpbGy$UwnvT`iIQ#Y$(T&fen@(9vz6I>D|sp_ zxhbBdf?{lY`>OL}|Jo`~U$%3v1{B9Rc1MhUPZw-0;G9{0TVF==P9=;Fj#@5E<0wJAB zSFS+ynB?%Qs06M}8{b_|T{p%$Jq}a(;Rt!Q^=#aH z_(B0=jX@>PH<7B~kS^z*vR;09&nj;*{=QNTZ`#j{g-k)BnG;l|!W2*zXX4cOo$FV$ zu8qP6^0jtWtmX{)HJVg3d7c_n>GY5@t>z}tuleW*-_71XRaw-pA4NGTA5F&xj9jPk zn5WX}RQQJPyi;TrL zizg-}J&uFSpUn|&h+2*WE{vLPGu?K=hleI-=9e7hGmyKtZQ@ejm%lrGq;#}X%G56T zVuFd>*OnrkbV@9juu9q5rZ*E)#hl?AcfiJo$s$rIUpCFcpuFXE@KJ6tScd}!6k&?~R%K8MJ@b}1O~ z^o+?>DuPPZl3e*lhU1rN&!UQrNJHnoG*^FJAWpNKoVN`FZwsL#qavmPo2FV+$woejUmVCT((%c9@$ zywkoNGNOAfih|KHb?U%U^k)$8(#D1^Fo_0p6|;s#s!Bs$!a}v)P<7o}a`#hum z;)q$a0Vg1<(evQJg#`b+N4AQV&W*a6C7_gc?{>!h$T!@fZ2sq8 ze+e08FJYAZp}Dni#vDQ2)NYuFXe+@Q#qTQjJ+w(IKV`CTt0Be9m4fiWEKMBFeogen zez5MRDu2_dnzMJxmNCt2Q(E(MmYn6=m1ePTu~vq?DluV$H1i$yX6>w!bE=J;K`KFh z#C#;{nW6<3m(+DKs#gyvQBDPs;#vK4Z{y$JnGNB)0<)#beHe)NByw^>Mi6(jznQ%_ zN-o^pdzp<*miOuUmlF=?Zuk5#PUw+@mU-HuJ;m0UBQZi#@vCSJ5nkYV`>0$9Q9kHR zidc%_*bXtBJA9IvRr1spYk!KpZPT65Ta{n+xdt09h&NaZ&N3e7*f-9dKS}<~@s6>T zsLxa#TtQKHIo$|94(p2J^OZZ&98@OPS8Uyc7$Ea9z}ygX$IM&lsGd;yerzmSBOf=8YxeI62t>s%n9u6f}C=or~_VxUbDDo9l1Hq zS1&Y=77l{D1es`Wf7&2?>E3Ei%oDayOBN-3gAbp!vp3YkI9GHQpT;n)jcU9T(vrM? z@lM!I4V{AkPxHZp>+az@EvV?Bad!I_X|tW=osX61vbX3g>gr`K#KXFhgyANwZ8jK9 z$d-Gb@?$rv^9bYfpm>Bhy}~d9rGol!YP6^?POXd}i(>PpfwE(L^wB zhP^7QdF-qwYkX5xui$A!*?}2-_F}OprYJY(yD)=!s?etdq4%Ddn-@CTxOSzbf1tuq zf+cI|&hhkEeN9*E8+zzfT=>hb$(n<2rx2KPHzhn1+|T+BYAe)~Iae?8f+nX5P%9 ziDTsb1<#u7k29)@jIGwId(m5;<|8<(c&Bg*)*osM%0^lSYVDZPPwhyfguFLZ1;l{IMQgmfoF-l5G5RO8kF0{XM zI(Z(sRTDQ;#+!7)Rr9#{&Bdms;&Ro{s#;M|21;S-uG2zyQ*5mjq=_4;U2(Xmt+?ev z&uPci{7?+An?L)bmWN%zsZA=&Q!xRm0%f{{=2)#qNi#uTitUSjCHa`wQ10wu63 z8?K8=X)3BQeIdXB}j(V`C+-S@(?ha|6ZiZ|qcy&Gpk~db- zG8J;pe2llV;1owEd3JR9jca19Z*-0i%^8a8r=K&Mk2BZ5#PQ@N{oS_*>n+xXbUoaj zxw=ElrVXJ9-aLV{@fM}G0CI}36MzyO_AV>*Jj10C2N&hwgRZckHYIY?(%0=f*lKc{ z8jd!Faz0n`2y@&g;;0MaTM$});tQ{K9rrh^+=V){pNyTu z6-9ft_N)9k`b5r+H3tDcq!7Pc@4e19(o|nRdvM9Ef2^quzM^7Go*4&EGRn}5<9OBX zm`Vw64)Y1};yI-!K8PbEE%-8jrO=G%xyP5Cs~wY2kT0sIQ}{za^Z4n95%!Ll=bzEL zE#IjaRUf~lI`vYsD>vGft2yF~QxanQ$w|soh3e*OS6wlbtQktha?#i6bJ6jg>M}z_l{_7vZDgif|;9QT%MZB7VS@6 z)sZq6;~H;b$b9Zj=M;Fp``8&7ErykfT*k>eOg-@TA0+1z$HFjEvO7Yf%m=gk(dp%y zPZ6S=V>hplsVP_cq&R)H^@S)OvD%jxV;B%D7|7Bq>3@{yh#x%X4`2StwwlK!-RT-C zK%gM!B|d+FHE-+Me86{`?iF`;8<`F7`;dN@qBMe7uN2`19ogtuYu3Q2AB^Z?94#ZI zoG(Wab}4&S<{fRCOM76$v-%~z+0*Sji7FQ9B6F)ekNQtI0E<6sFVVSYGM}%O`>??I zU8KQTOQrhFg(z|zOP`-pDa(F75C}@-p zoW`kEl$9&r87mbmHrg$Jo2jMyPRlZW^zFM=%ow@Vw0X*n;fuhnZa+VY zp0l?~SZg>wkgp?>Z)J-LL?uP`Ugfk@b`?i^V`i`@lVNkITA)pu>sJZ~OL@ z{r5>i%rHkr6ZM_uZa1yZJULjtE(te=^Xsd3S4HF;l1BB%$L*OoruytP#$(67?m)7F zdpB+3cf8f7wi~kqEY1o__sDjMz#uP`^`S?eIM4i0lFGuogO-Q9mS~(Th|BZQjF`?} z2#O&y|9mWw?T6u2kByFoYk@3l8rwwOb5#t|LGQja&8WigBHev?8+ANfZ*M#-dHNXp zy%=h+A~Uh{lR(7quX-mAP%YIE$t_-65b- zJEUB@uX<#ZpGCCnn1&4ZVkgGiBtzP0w9adH-YP6W%r6g5_NuvaK?MmJRHLX@uT2nZ z^;f9$r1kS`*i5O=dP`XLUX7^O*OIxhl@yYs-3X0!dn|4-p)$TsAgiCBa$9NZ4A>f- zpwu>I8rO!4->D$jOhrFzO>JFazZg8nx1pY347r=9*)?yoQB(POfievFrEu$xe6SZl zwwzR!qkp@4Iw2Kpm7T|``e-u#G?n6cUKQSNOO-`?CL@TGC3Ms(7~R0VGLK|6#6oIj zq4&37VGBPFYPk9SW=a7Ex4sB@s&2HRB<0aFkUdKKZjHlf?;N??D?j4b-2(TH8`kEXq zb@$!*=NM^CHLW0QZlL+{`1eIz$M)wC$3`xuiV~>^x35Q2DOqvyo$Ui@axY}(Ug(zQ ze_47)6QvoQE_ao}HZeA|sNa<}U?f)O{trHYo=Kh}T!J0DC-X+6H#wovCtIcHTJJ$`;*-hbfW7wd z0`^jt8_4K;E2@grnt{eZAAQ3cZN^~Jp#WRPf`N&iv+OS%k|tj0ewt$(NzbiMWkb`J zt?v93zF|EmkLaOqtYyOw!)qIv?UpOBnbthwjp(eXhN@M)#=yqKYGeg0bDBzT=iHBS z%@Zm>3gYfq{=w+=qi&%PBDo750-bPj<3&hx+o$q6r!NuiQ@d`(ZQ4CSTo{;*{cF5YAdQ`<` z5^a;EskeFek?9DLSD(>E##?bGYcJmsmWWFFQdl0qbX=n_L}J7vlRM+v7-el=DT8jf zK;B~p7nai=2?=d-{Hgu=9Oy2l?|rwQS7A+X3R1G#y`PQ85}P5ZXt}+`t!l=_#!#md zu?}KFjrF_=l7ska+(o;y!Sah*c>?N%MT-NEV49KCCq^x9)Dbh|9mftjb*u#OlU0vo zNHT*-F3Wf^L>h-cc@A`C6{x=S>n$k3Q^l%$=5Tm=~|D zu;d!ZK(;L_RU1c`h*5C~rSN{ZmosBM@03F8k@D6~DNyRYY4RP37K)Um9I?ohlaIsl zjdAvfZ%bQ{DLJ_ux1sYgewpMrM%!GKssTK$D8!CI;{0-zIALv7LCG^a32;W*Pu(|bsGH|GG=-IUJjs4`Bh4%g2`^X|b>H${W%33I85^Nt-z z(Qu|APVxKE&K1fAKRUXK^Ne4oXUgsw#PxI7*Dn&hr z&Zeu;U0p1M+R*-hv|Kk^o>=WXDSuL3=($C=>S=ZPY37dg`T>unoFYjJkqV;v`@XLm z;yseZHoM|Axg~vD{$qMx%}(-XxlW377d@9`Hd}o`wdz`6d{Vl>uSD_dcbN5&MHch?@~&vy z_M@Kmk=%i6u|CwtHTJqyHT-dK#APIr z-v#N8!m2@+Ue-hTQLbuGTX|!cF-`~Rfzl7eBP|0>tPp{h5lXI54Ru;oe`S9Rj`S?R z4_>rm3CjLzP`Im?3sM=RNo*-7{~?5USq*AuY{swSg-7zsOUg^apz5^zs^AU1vW4#1 zKa|0d8q|YG#3@Tj`T6-t`pHUq;oYUAm6VjEU@}rNG7=y{f)Icu!u=(%1VNICUp92X zOLII5N4oXrC)tF%c=-_3piofG|EFgd++W7A1j$1$B@td2DStRlN?H;o^>;AN!;9!e z@bLOCDu0RprHJ%L{a(XArXflG1t+51{tKKW`4=3FwXyO4y$THG9}6K6^?boV{1McD zIP~Yb3046(q?84c;N^oyAoYBaSfb!zEWp{6b?`_y5vdFQ)u1vk8F>k583`FhE0~h9 zG)!4WK?0_z41@iJHuiEwxdr?uw2XqXtnB|rgY^O4VZ;AF_`f2qOhW(i3aril+oj)1 zI$mfmJm?*`3fV(%e~S!sbj-k~9~2st5l9a{26{RQ(n<;n5;Bs~hgeePl+957NVJ_U z_{4-IfK{djJ@d!9e@pHECY4i?`>lI8yCt~2z$t#2BxP6G00VbN+M-;E9>1;q78oJ1 z?j*^dmJTJPETMf!Wp0f{fq9iVoa9hs_ge-o5(bm{+f4TVmXWp&zv^K@DeI#ML@#{6 zA?w$EvP63S8vPogQHL9epZ~C&l;H@H2Wn98$;{OUfpk4|0yOtaO7L#0Df6X*dGD?3+m^A1kL<2iGQyYsXtsIz2qtX>65C|zr0pG1pMs~0QG+H az{3SRrKJ8kroh2p2OSa%RuCRM#{LJM{50JF literal 0 HcmV?d00001 From a17561bbd38feb819a2fee840233186eadae22dd Mon Sep 17 00:00:00 2001 From: "[Ikelene]" Date: Sat, 27 Dec 2025 09:28:29 -0700 Subject: [PATCH 5/7] add server storage extension --- src/lib/extensions.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/lib/extensions.js b/src/lib/extensions.js index e3b79b35..a102cd80 100644 --- a/src/lib/extensions.js +++ b/src/lib/extensions.js @@ -470,6 +470,14 @@ export default [ banner: "Gen1x/cats.avif", creator: "G1nX", }, + { + name: "Server Storage", + description: "Similar to the server storage blocks in the 'Storage' extension, but with a better server uptime and non-global keys.\nDid you know the server is open source?", + code: "Ikelene/serverStorageExtension.js", + banner: "Ikelene/serverStorageIMG.avif", + creator: "Ikelene", + isGitHub: true, + }, { name: "Free Servers", description: "Here you can find a free server for your projects. And also check whether it is working now or not.\n\nЗдесь вы можете найти бесплатный сервер для своих проектов. А также проверить, работает он сейчас или нет.", From be209d15d81ebe361a7fc35857fe4f137ca4f6b2 Mon Sep 17 00:00:00 2001 From: "[Ikelene]" Date: Sat, 27 Dec 2025 09:33:46 -0700 Subject: [PATCH 6/7] fix text not fitting --- static/images/Ikelene/serverStorageIMG.avif | Bin 7415 -> 4466 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/static/images/Ikelene/serverStorageIMG.avif b/static/images/Ikelene/serverStorageIMG.avif index 90ad280f0aa9962545d0128a803ac4b1590fc7fd..3b8e871ee5c9f123d98c04aba031d9554b77322f 100644 GIT binary patch delta 2336 zcmV+*3E%ejIr1WqcN7RB00IC2000Zq00961000001)q_NmJ1*N6=7p@a7|N2RFh!= z8nZ?LWdaus$!%m|bP@mx3K=Fb|8E2YG#U^xkr0!@10;XiNH@)^LtX(EO+INr>Q(sm zaV|RuJcOGvl`7eNkIE&@>yyk$P5IhXJ?R`jf__0zbIR;orw*sazm#Vn(_zG31|747|g-LyMeL68MW%uAh5!JpFC9MW2R zFx1!2Z9Y!f^#e8VUgUll3>Fc)534`N0D;TN)8n!A!s7DXN`>RDMdEX(Z=y9V@hp)$ zp%9S=nk%LMl#|iBa@Mp2BNyb`b$GieuX9#AI1qo_a5BRaG8{yvgLwOnQW-fc+Z$;L z5z6=NTHY7pBxaEqp|eL}3xcoxRbqPFHdhyjw;#?j4T7vbaW#LovacY-6)^(LUD`X0 zn>4I(YlUQU+QXvw7rQcEeNg!G94&BR^N`jdM_l99HZ}W(AJt{~A((YtSofk$VdY=IpqpJ%0!zF()Pg@w|WG9#qR& zeay!=HJx!bKGv=vRlu{Sfls!+6a&QVBMr-q>>BneP+%tKnW>>^Wg7#;<>4S0ajYz` zRWopB|G;Itb*|o?1yiNVNy0(Es*GO0pR+cI3bCVnpgcsC| zTNB0k*^^GLLV@0&dv0^l;d)@5#|*8&+@*%!kebGbLypC|ZZgeTush>{)EX^pT24Vw`Y_9aP&pdBnudR70n~_hK%DN_}qV7AJ1rbsdh2x}!p| zdA7r6?w7#lv96GGc6Px)^150-Pf^b}v z@v!NH`m{=TMt=J#!70y($awwuF=~exasKsupRd_DOdpvaIT0lao+34HUIFO3K4|pL z(4J{PyJdV8_SPl1z=1%6#7A5;e;T_t)84E|dzm4GyKj{;XH?ILr{hr=6GfykUX{De4ksRe9bPhT6+i|hA#)y{S#IeV=bGcJ0 z{3lEqd_p1`U>|=C5CdDOmuT?_l}@IWjhh#n_cigHQmOIh>>IK|0~$U4!%#-%NGEay z$JvQFnO|C#h=hMy-FK=Io@Avcd1O;HT#~-i|03GbJwVth=lqK2Md=1m(ucGsT3NKI znv%=`)#Rpo+F3XO1nm{h1~Uk=RH2<-`QSu2=oOf`H$lx$2|cEZ=Zi{7H>m{d8Ml}HRw0(c9(-wzxAQ?$+H+p;%BT#8F$H@LijtZe-{R)=MTvi ze3fG`xP{e=WLxzHzd3+%1wCft2xh6@jI+KIz=VG@HnVRN!5l4Wk2g%m%>)MMC&ps} zmVy1{23~Z@LquVn-i)SX+8=dARr(- zFLGpNIzeo7J_;ZpAUrQ*V>)zcbZlil3LqdLJa}zzO-(vTX>)XMa+CcVhXpe-Iy5+w Gi5zY`x=R!Q delta 5306 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 MGB!FhGm``nZVNdtC;$Ke From 9923042ecf3bb882df9d6093e469a749e4664b31 Mon Sep 17 00:00:00 2001 From: "[Ikelene]" Date: Sat, 27 Dec 2025 12:08:04 -0700 Subject: [PATCH 7/7] add blocks for offline editing to server storage extension --- .../Ikelene/serverStorageExtension.js | 163 +++++++++++++++++- 1 file changed, 160 insertions(+), 3 deletions(-) diff --git a/static/extensions/Ikelene/serverStorageExtension.js b/static/extensions/Ikelene/serverStorageExtension.js index b2369c12..59856ebb 100644 --- a/static/extensions/Ikelene/serverStorageExtension.js +++ b/static/extensions/Ikelene/serverStorageExtension.js @@ -6,6 +6,10 @@ this.serverUrl = 'https://ikelene.dev/storage/'; this.apiKey = null; this.maxDataSize = 262144; + + this.editMode = 'live'; // 'live' or 'local' + this.localCache = {}; // { key: value } + this.dirtyKeys = new Set(); } getInfo() { @@ -44,6 +48,11 @@ } } }, + { + opcode: 'getAllKeys', + blockType: Scratch.BlockType.REPORTER, + text: 'get all stored keys' + }, { opcode: 'saveToServer', blockType: Scratch.BlockType.COMMAND, @@ -96,12 +105,47 @@ opcode: 'isServerWorking', blockType: Scratch.BlockType.BOOLEAN, text: 'is server working?' + }, + { + blockType: Scratch.BlockType.LABEL, + text: 'Local Caching (Faster, but not live)' + }, + { + opcode: 'currentEditMode', + blockType: Scratch.BlockType.REPORTER, + text: 'current editing mode' + }, + { + opcode: 'setEditMode', + blockType: Scratch.BlockType.COMMAND, + text: 'switch to [MODE] editing', + arguments: { + MODE: { + type: Scratch.ArgumentType.STRING, + menu: 'editModeMenu', + defaultValue: 'live' + } + } + }, + { + opcode: 'downloadCache', + blockType: Scratch.BlockType.COMMAND, + text: 'download all keys to local cache' + }, + { + opcode: 'pushCache', + blockType: Scratch.BlockType.COMMAND, + text: 'push local changes to server' } ], menus: { serverMenu: { acceptReporters: true, items: ['global'] + }, + editModeMenu: { + acceptReporters: false, + items: ['live', 'local'] } } }; @@ -113,9 +157,7 @@ this.serverUrl = 'https://ikelene.dev/storage/'; } else { const trimmed = server.trim(); - if (trimmed.length === 0) { - return; - } + if (!trimmed.length) return; this.serverUrl = trimmed.endsWith('/') ? trimmed : trimmed + '/'; } } @@ -127,6 +169,98 @@ hasValidKey() { return this.apiKey && this.apiKey.length > 0; } + + currentEditMode() { + return this.editMode; + } + + setEditMode(args) { + const mode = args.MODE === 'local' ? 'local' : 'live'; + this.editMode = mode; + } + + async getAllKeys() { + if (!this.hasValidKey()) { + return 'need api key'; + } + try { + const res = await fetch(this.serverUrl + 'listKeys.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ apiKey: this.apiKey }) + }); + if (!res.ok) return '[]'; + const result = await res.json(); + if (!result.success || !Array.isArray(result.keys)) return '[]'; + return JSON.stringify(result.keys); + } catch (e) { + return '[]'; + } + } + + async downloadCache() { + if (!this.hasValidKey()) { + console.warn('need api key to download cache'); + return; + } + try { + const res = await fetch(this.serverUrl + 'getAll.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ apiKey: this.apiKey }) + }); + if (!res.ok) return; + const result = await res.json(); + if (!result.success || !Array.isArray(result.items)) return; + + this.localCache = {}; + this.dirtyKeys = new Set(); + + for (const item of result.items) { + this.localCache[item.key] = item.value; + } + } catch (e) {} + } + + async pushCache() { + if (!this.hasValidKey()) { + console.warn('need api key to push cache'); + return; + } + const keysToPush = Array.from(this.dirtyKeys); + if (!keysToPush.length) return; + + for (const key of keysToPush) { + const value = this.localCache[key]; + if (typeof value === 'undefined') continue; + + const payload = { + apiKey: this.apiKey, + key: key, + value: value, + mimeType: 'application/json' + }; + + try { + await fetch(this.serverUrl + 'store.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify(payload) + }); + } catch (e) {} + } + + this.dirtyKeys = new Set(); + } async saveToServer(args) { if (!this.hasValidKey()) { @@ -142,6 +276,12 @@ return; } + if (this.editMode === 'local') { + this.localCache[key] = value; + this.dirtyKeys.add(key); + return; + } + const payload = { apiKey: this.apiKey, key: key, @@ -175,6 +315,13 @@ const key = args.KEY; + if (this.editMode === 'local') { + if (Object.prototype.hasOwnProperty.call(this.localCache, key)) { + return this.localCache[key]; + } + return ''; + } + try { const response = await fetch(this.serverUrl + 'get.php', { method: 'POST', @@ -209,6 +356,10 @@ const key = args.KEY; + if (this.editMode === 'local') { + return Object.prototype.hasOwnProperty.call(this.localCache, key); + } + try { const response = await fetch(this.serverUrl + 'get.php', { method: 'POST', @@ -241,6 +392,12 @@ const key = args.KEY; + if (this.editMode === 'local') { + delete this.localCache[key]; + this.dirtyKeys.add(key); // optional: also push delete server-side later via dedicated delete api + return; + } + try { const response = await fetch(this.serverUrl + 'delete.php', { method: 'POST',