From 201d89e2c4cf1a070e41fba5e03580470b0238e1 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Wed, 25 Dec 2024 21:39:59 +0000 Subject: [PATCH 01/85] feat: adding torbox integration --- src/main/main.ts | 3 +++ .../services/download/download-manager.ts | 14 ++++++++++ src/main/services/download/torbox.ts | 23 ++++++++-------- src/renderer/src/assets/icons/torbox.webp | Bin 0 -> 12150 bytes src/renderer/src/constants.ts | 1 + .../src/pages/downloads/download-group.tsx | 25 +++++++++++++++++- .../modals/download-settings-modal.tsx | 8 +++--- src/shared/constants.ts | 1 + src/shared/index.ts | 2 +- 9 files changed, 58 insertions(+), 19 deletions(-) create mode 100644 src/renderer/src/assets/icons/torbox.webp diff --git a/src/main/main.ts b/src/main/main.ts index add619e1..81916174 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -11,6 +11,7 @@ import { uploadGamesBatch } from "./services/library-sync"; import { Aria2 } from "./services/aria2"; import { Downloader } from "@shared"; import { IsNull, Not } from "typeorm"; +import { TorBoxClient } from "./services/download/torbox"; const loadState = async (userPreferences: UserPreferences | null) => { import("./events"); @@ -21,6 +22,8 @@ const loadState = async (userPreferences: UserPreferences | null) => { RealDebridClient.authorize(userPreferences?.realDebridApiToken); } + TorBoxClient.authorize("7371d5ec-52fa-4b87-9052-0c8c96d947cc"); + Ludusavi.addManifestToLudusaviConfig(); HydraApi.setupApi().then(() => { diff --git a/src/main/services/download/download-manager.ts b/src/main/services/download/download-manager.ts index 80a3f6fb..1c91c4dc 100644 --- a/src/main/services/download/download-manager.ts +++ b/src/main/services/download/download-manager.ts @@ -20,6 +20,7 @@ import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity import { RealDebridClient } from "./real-debrid"; import path from "path"; import { logger } from "../logger"; +import { TorBoxClient } from "./torbox"; export class DownloadManager { private static downloadingGameId: number | null = null; @@ -29,6 +30,7 @@ export class DownloadManager { game?.status === "active" ? await this.getDownloadPayload(game).catch(() => undefined) : undefined, + initialSeeding?.map((game) => ({ game_id: game.id, url: game.uri!, @@ -294,6 +296,18 @@ export class DownloadManager { save_path: game.downloadPath!, }; } + case Downloader.TorBox: { + const downloadUrl = await TorBoxClient.getDownloadUrl(game.uri!); + console.log(downloadUrl); + + if (!downloadUrl) return; + return { + action: "start", + game_id: game.id, + url: downloadUrl, + save_path: game.downloadPath!, + }; + } } } diff --git a/src/main/services/download/torbox.ts b/src/main/services/download/torbox.ts index 3eade81d..1ef57768 100644 --- a/src/main/services/download/torbox.ts +++ b/src/main/services/download/torbox.ts @@ -20,7 +20,7 @@ export class TorBoxClient { Authorization: `Bearer ${apiToken}`, }, }); - this.apiToken = apiToken; + this.apiToken = "7371d5ec-52fa-4b87-9052-0c8c96d947cc"; } static async addMagnet(magnet: string) { @@ -55,22 +55,16 @@ export class TorBoxClient { } static async requestLink(id: number) { - const searchParams = new URLSearchParams({}); - - searchParams.set("token", this.apiToken); - searchParams.set("torrent_id", id.toString()); - searchParams.set("zip_link", "true"); + const searchParams = new URLSearchParams({ + token: this.apiToken, + torrent_id: id.toString(), + zip_link: "true", + }); const response = await this.instance.get( "/torrents/requestdl?" + searchParams.toString() ); - if (response.status !== 200) { - logger.error(response.data.error); - logger.error(response.data.detail); - return null; - } - return response.data.data; } @@ -94,4 +88,9 @@ export class TorBoxClient { const torrent = await this.addMagnet(magnetUri); return torrent.torrent_id; } + + static async getDownloadUrl(uri: string) { + const id = await this.getTorrentId(uri); + return this.requestLink(id); + } } diff --git a/src/renderer/src/assets/icons/torbox.webp b/src/renderer/src/assets/icons/torbox.webp new file mode 100644 index 0000000000000000000000000000000000000000..68d68531b4ebc3e12028bedd4d3946060d4196d0 GIT binary patch literal 12150 zcmWIYbaTtoXJ80-bqWXzu<%LJXJF9(f3RL4lv{awuCnE?|LZ4j`?ltK#|(zQNpFH# zB~zHSJg+|!eDQc#Y9Cjb9e-Y%LKJ^%cxI+ni& zeo5Z>`gi+VyVsj%ZGQK>bjSDq|9{ukZ_dBHtv7OhWchKwwr#n$zpd_;GqO8xIF}7a*N^4uMZFlbNZMnCD&sC&vocrd+Hp|^zxwoIK+a+`CZDh`7Q=YOM z>(@NV|B`OcxP5Gbdg&*IId4|I$;_F(_Qtl{d67BltBaPeH1fOJ_jXM}#%;gXJenSp zmUv9Mw_E;(=WW}xv^2MFt7C0iuctfqsd!FI*|tC8@7Ea-ZOLi-EZ9Od9RAnud;4yB z{F^1)a&Pa=zBM!R|FND|Rw?1-Zn?L&_2x$A-rn|mE`!zI|ND0dyLar^`+eTM>i2uU z->cs4wfDQ_^FRG8EDIhaGj57!-*~WlWAQeP%9;cXrcX&{j?A&OOIjv%z(K^unroM+ zTZG|A;fOv-2W&*WoDI^w9} z+4Us;^!pDw?{((1gtK@u954!6`}6R>|6#XfZ*R+0jyB4ioqcBiT+JD)S>(R|4~?E? z{6A6T@ak}%=SIfH_mkSZTfg5cZrb&I&fe?yLi5)%mSmUCR=U%<@d;yuzOi+3TE{Ns z;3F0D6}Of27=O5Idc1FkrP=xuev)hl&IoR1zc6X$Oi5+_1(xNXdCuhdoz*+vFmvY2 znOaq!cy>LJG%0QgEDv$Nwb1g_jWd1oWwzQd#k8Gxc%Nr#?#VaDv}KKrTVkp%dY_GW zb!Ot%ZBGPsR{986Oq!|qB<tj5Yf&?1W(%<}cqqO6zkcStx9{`>4(1i+u!??I_g^@nPM@pW$o1x4;Z!xQ z=pEvsqNRd&g;+oOOhhjndRKI4h&*uFxX5i$=c3~qH!n(Gba>IS z-i5l73tfEE)OLyO{&6r}Eh~sCYtf!X-(A9!!t?*td^#n4&ZW{tzWblR-j2H)zUA$o z*<+Ka^V2WoAOF>q6!Rl_cRu`Hl(pytgW4)RDTT)|Z~7J$ix&he^qvxJ5>ldA_B>Ad zzIsY$kZZ2%>a2~suj@T(DYFRaja<0Lt1GPg-Epg(+ZSzJ_V#I@IV95^*K6ME%-j(YS=TR_6JVz1LRq9fzRU)sq zzUX{$K)-8|)42$GIY4pxXL7i~ieH_#9;y_kAIl=JN8hV6LET zZ>Hw0S=X!2OU1beE?TwdzAJxn*EYTH&%>CzS8Q6b{zZw;&Fs5ZA9HD{moqN~UF`4ai*<;*x{0Is3zIbKXj7H!8yI%P73hORPTQqagYeQxKH|fHr-ewOO z3M&@%y_YopVjKSZ1C#UC{uRv`>A`2NESqb;%VpBO?{t}Q-X1dv&C~+-lZX;;^X3)^+)*i#Jc_U-`UxOZrz=$wfc6zWB0LdDqda z*XN{0Z^^hf*X8;C$*rd|X7*TYwcOL5*AaK)!-e+c#@2frF7A8YKkvt%{aUX|ib6{@ z-)_mM3I1}Tak*-hbL1r7l0{bLJAPg?HviA&T51;>=wj(28@5Ai@8)fX+{>qV1yvoj z?A*51O+VLmhvi9+e__0qTg$V1@7>aR%rANN%3{|L$1C+w7qgdtJv95yBB}75tM{I| zSbTbK$`{{|7+tSk@y|;xcF+HkB;{>2G2Hj!sb?=vo(-NgOT$xsl`Wf0=$99t_y?`wQug%O;y{{F-za3>c=7T1+LuF-af3`R{!PGKYQ)PFVAN>tn~l4 zy)!&px2kIG*M+~Q?8$F;o!?Pr>e8S8EQITJ&&{mz>$P_?8rvu3MdE?{cp=U!0%4zg_kIq>GQX zMlIIbwr=Z+)^OR$E56L{IOkG&=Wp$v3CW+-0u3+nU(V{{x@n>-X|JO7%zeJqdHbHM zpY4@fPio(4&I{SH#8NBmb6r=|`4jn{x=yT4xS!vB%JKuh%g@zv#;KC=HMb9}n8&>T z?ODF{Pc)k~=7b+!-)$9jPvGMJJt;R`c+39yEt+-jWZ(bin|`{zlxH`&zG%_4S?qt3 zPioIAwc8uLc+<9T^UiOsIx@fOU)8@A8_wHXOXgP{S}r#|<3m7TW_#70$G<|pYww-9 z;&1cx#kZ!J|7SA0`PGHf|L2mkf#vz1{MTQ9`7Pn-I)hd3MZ2!)-n!ZMQmcAjV8}!B zSCvzbKS|l7sr5?H>gS)p&iVB(x9w=Ju6_0ON&Vaq_t1Nd`M*N8-#YB7Uz5Ms=>6M6 z;&XF^?yl52r7CqJs{2(;_T9euYpgDvynA@Pnak9?A6Kg*_{FW%0^hFhu8Ot39h?KWNWY{r4PR~8jrf0uHg$Lrjt!`-$!uB~{mmVNsT*|I5r z)=!?j{j$i^Jul1N-Es{3-%}Q9Hhs~eZoRj!B-ieXo?;jJ<>%#Ti&ssr?yM7ls?TXtOm%d}3rm4%+d;#70w^DaFoL*7CyCm@K`TkPWJzCk5_!zskNo z*Bsmz|yEYw@^Au6F`_sTG*RbKvY^0e>e z(eRiOL$-7KV?K)8FB7>vZ{GE&zJr@<*4j=ER9n05yK?n$hg-ju*PBn>=egwrSF*w0 zL;Q1}_gq?5WxhzC&w%afv(u6NS*uhQUtltS@-sQ1_8)KLf)xQ(CUFNRA5-|fB>ch* zq2ym*9kdHQq8>)gSh;qCg-Y+&*Gt2+?tK35{lD@~&}mDCNy?p~=Z-L*I4^9_=U;Z9 zW#0aEbI$C|50MhFU)9=Q`ST#}dg<2&uL2%v*7OP-|8|wj=fP9%fQlbHCvKMusquEn z-Cg9q|HVZv`Ne6iYS$Wbb1Xk4x31e*W6%D$_O*o1n~&-a_ZT=Q>|4&-D&PF9Vs>_D zwZlRKQ>Rald3s(qGaF1yf9JS#?IfiH)#+FE&tJ2lr&%K)LdtPY!^Zl3tin|fr9|ZR zHEr`!jXj{HmlhI#W5K?VhZ$?)*B|&1Z_g|%U&)v|)o&Sh=hb)h!BOY^Of>%V7(A1I zmSB@VzoFCi8^+{=VSS(v{3%Rh-So z4r=*YvDmbVXXnO${TOiU^xrK@r6ZoY_PhR}i|oHg z-m$PYS@-x6F5zp@UoOh<7uWPn1bmC@z{_Xx>q6&-MFt!=g;L zppwkqkEXj#-Z)M@)Ohvs9yx`5a-M%!mIm{#S)L!Gb9qj)edC?`yJqY=wK?%hIlquq zxB7<7%BOBsb$LDg6)kYq3Jl=Brrt8Y&5G}|&)r|8$bWp^F^-PQQBojEJbPG#q` zikZtzt0z0H%gfz4@6D(Gj;Z<$UwJazwN>9La6EZ*?bg0klNa`cKhxoC2$i|AV$+6+ zHJMND^;>m(-g1cX>RzX$QbwPz>*lVQ938!X@4gRZBJXZ9ve+_D%a7*g-kT$T;>o$N z9sBY`eG3Amd`h@*7}CP&P87vm|4viZ?NafFuv^3WfzrgqdQ?$ zt*4s!mThnJcgr~bv9QQ6y?wxoKQqBPLRFK!QT_VnYu`fW^RUL-csw{DBo*=0u}6wq z`s#GIyz9~X<+WZ;jj3pHgJ3 zedqt18kV1bA8dHSe$Be#&6;IO*@nx4jyO}^kXzYz0RvTMm#pkY`p7=dM zTjs((V~2H$b!pe8t$!L6#{W94;YpUtL++A`iW??*zq-7BTGTm4fs<3$yxUf4fAr_W znVTJ%*SvYycD1)uLilv=yc&)#Hd4$o7d*E!>^9!Wm#f^(Et@Sr+hW1&b&gJ-D`u|_ zt#(w~zOgFeTDWb~7tX3zG6}YSkKHUfd*W2=n(oS(iCe;7=4@cJHr}Lv*H1b8I8 zsv;e8YacLtyvpx%YWKviU*3v)>ps_I-B@b)_sR3an)%OPN-!o|Z=b&P{UeQxwcB5; zwU#?mb8kBHuJ>LE{r+0aw{I?Sc$*eqR=(!f?LKy{_upO|NV+iBBjaBF=RL2aS<}jI zFPfcu@qYcBP)1epM3t!Kql=syr^+3gDmrUgs6kux?sCq~?<+Sqe_x>GEGfa4knXm1 z>FrFB8vomuH%gUOnm<#_x#w~Jzx{njQ3>YroCXR9*7@q%9v3O_|2?_+&~sPb;&=St zWz+8m?mX)~QM25(Ai-n86aV^s{Ha_U=JS02EW7l1ecJx3_YUi5e*JsCIR2Q(x~Gf_ z3>mLkXUMFX*0QEa>(1@1oxS`0W~HsQ-q{x9RlA}7>tnmVE@g&n?cw#FR% zvc*V;@zH08GfAn!0_MGPtvTe?7Pm&|3&X!&p2Xa%V6RJ$|1Y;rbYT~ z|6_OO_+D?XdY@{&z1Al)7UX=}(7iYzvBTkE@)Y?q9ox3uTjb|jckk@J7cXO~*U264 zdpxZ-!kKyd@{m;;I$aD49GSw36(;Y^XP+VR^XuiZ?Yix4dfvCM?TOc0IqBPt-OemB z4T(;Zs#EuczVYSy`N_ZS`scYR(;WV~SD&Ax_3G}*qWUB$jes8#s_jM#=B;_MCGThc zj_uXb^Q-1>(un$=#T$ApcG`OW9SfPl3Ii6uI;8ex$;AEd_Out*Hf~+Kp;Gr+z|yCO z-fQ|MNb646ZKhlPM_<17)^b@HEBzeF;<_CrD~pQb+gY^4+PFEFi&viX{rSYd?RWL} zmpk9zT)*|Zh4(6@*`_a&I;;%i4pgy!-+Oh|l&SJ<)jPLs{5ah+xN3d&id8v5`)UN8 z+1^Yntj*H4xOC$Eylw5Zrb=b8rXR%CEH@2F-EB3^GT{tE@8|SsTWb{Jt8aZca3HcS zH2=@LrAt z*DjtB@lWL4%UdpYZTsuJqRWbST_>FVEpeGe%)ID;-@jtdiT`chM)P#XOf8@D^R#1- zO621?$1j&Ay_s-fi`}!RvvMIP@(-oY4Zb90x4U|ph_8H(U5$tvb2IyzCzF1i?Y_V3 zsd||IZ!N`tcC~R8-@5`sZnijyb~YX3zrn5i`xG1JoJsXxuDx@gzxS$dwfq}<*XwJ0 z0u7oJzQ3AsXFD_RB>jlvQYZGycK-Hy8u>Yny-h&Zg!PVu(#gL@_g8;wa=N!&Z{hwo zpH3YtX4*XEo|gE+X}xExN`0C+-t26AnxMKo<>_0&Yu}ah&o;C0z1ej^CZOf!p6iCK z>4k?m-h6stxVh7IV|v$VcAYP=l9;cVISYz8k-xv^s>sd73O}DUi52th{<1YsdC~Oqvv~jHMZuZHd>l2@+j*4XuNsr8@H)@lK=iP?Ng~~x^MIPOzaQi zpMAzor!TqoDY8y`zj@uXD!=V}5?g*F~gF`#j3 z{Fx8izE$h!+x!&xkQvX%k`dMt>~=A=R9Vbj_0S5Y-NrZB^gJc=WV07MC=+I9>T@}; zvAnf6Aj4~I-!bK+n2AqjJ-;2Z{-!fm>ZY%auTQS~CoiI6XkoH7tX*K!@gv0%bJ+AF zPO5&DS3j+~IC^GQ>+e4+5w>CHXp0de4Zoc0=VAgW)H_xUn zcyWp~K6uxSXUD{k{pQ~CsKw~?tHjvv)8qKm>zfwe3E!5d;*|4FyPtid!GQzwstZMz z%lI&*_HEtP|Exsg@6~1|K8^F*P9^FZXZO8N{?H+jlrXhXB09}%vv%mRFGla!KfVjk zVafVb__ArC$Kiq8B-t4YmV&bw=e<8>9eVHWSqIls3}_6$^y+HN0EvAL7^#JxjTstyY*lILQR zb@R44`+3z~&t>N}<-Ia?o3M}JsYFj$`Tg4*mHewth!rg6Tk~Ynx-}EFdY2tNAW(JxjPZ4ocL%ievO;FxKCv%^|8F4sX8ACS29xsww?FS!Yq|4N z{_=K~UYiZO80J|lYfpPyQG4*L#~=O$dCy+{nbuo$G4ZSLG>7ANZXC{t_MiXWxl?Y> z*3|l+PYX_|TfCTRrR;Wb>+-F$K6f!JILIDW#Ua1`Vapo1Ii-f@4MnOxE@)tW@qYEQ zR}Y%cYWSX*wjeP{q%l0cx#P@alXE{ZpGel^sWisQ z8*T49wrl%WF@L+3pN|~>S}q8kr?y3`#$QO*@_(@pQ>Pe5zv=>|Y>Vm}Ti;Eo-Jvoq z(V%heoQSr%y(_-x-aEAGZ*9-9I_m|o^Ok>&o_5H!{c8WstsGA{`j0K#J?~;>|G5=^ z3ocf~O883LKYxyQx2Njr<&}SJl{x1x$lQEoc18IlC6ldD@vM$#nGcAc466vv^8M^( zG(9LvbfFB7`i7eOr5o>T{-t&P{KE%*+bZ6ciaDKN|84%ws^2A}!K~jU^QBUC(z=)* z>NC7GOY3#qA4&!Vd~h}Sdck3l9H+wtvzK#1R^+tQ9N%d(VTMPc$HN)Pd3^R7vAZ;~ zqj)$?lzLVk>JKzGVE(8g#2_WSQN?ESZKIMqZ=XmzPqE?f5K>&M<@J0IPspWhHmaxRwy-_oMr3mi}_%h>LJs|Blxi-=$*gxvKnr;+vVPOIgg7T6q&d4LwG>R&9u%E zrGSSUQ}*7FyK4L8-rS@s{yUfZ%oIK%YV-PxHSd&H$KGf$cph53QRDmr-}l>eqmQn* zzwmmO>PwdBDK{4DO<5N5+E0Yz|Jt0LpIOdoAB=frKYN0%yi7ssik&tAQ?l|md8oKF z{6Dv`eA|ceq)BYAY*#2npV-LiqZMZ6wCRYnZClo#gTa>1^w0mFv&5UZ?E4h8Ej}@V zj0eu|t)4RNd6vz2&VWw$1CC0QLt-!NJkLBe#IZVTeYa{-qjm*Tvx$#{v%8#v@0%H` zI0XHbQ};co-JTE3lee6hTIi3)>@7(;0Z~dABm)`yPKH2E<0r7)Q*)JBE?2Ku=)KZ?Ny*Kk} zm1yzGM>$cuI~A20b(JP;(3)~QnP4Foj`oe!MlsE?3!A>jHUhcDy#F)_VI)Sl}27ZT~N;y zJ>hD!;*)opt=VkMLg7;#jz49*W${?HXVuwdA+PiqaukDZJiD`9Q1P0f4|q96jJ;Bj1ohKe#N#!v04{7ee+EXSz>>;3-+5BeX9PpJ>i{(9G_D`myQiME{w#P=9YY7tlzy7A1HX7_~&ZCC!jUwUR~ z#-`5JBH0Js_a&#F$t}5aZL7|vqkHBa?{GO}Y;|egj3q1HEx93DRO6WRAtP#WB8TaJ zzw&Eh>YfV%Wf#xRRn*+{?vVUMwJl9QoMOBWwnjx&mz6&^*#1o#!XWLcTHM! zT5HkEsc&*Li#0#xxUIe`c2FXzX4eM6PjVrDd+fO;hcvJG)pSqrK{p43-MuYUy+Wyz zvM%OEcG=8LPvZIax0JtU;gUeJ=QGutTpskKY_t5HJL%Wn|Yuk5!dmS-mdt8Xv-B_}mqCy&v9b5ITDhq-@7jj$gP9HHN4BMvMe}Z4_f+Fb+~wLt zzB>C}|DMd&oxH_ylAh_j)ywXEYMwFUu+QV@&-+`hv`w*(D4zSj#>8)S(E6*%vt`>J z_vP%|EP7SN`p}V@nQGgDS0yIq`aUzuf!> zd@J^yi1{+(`+`@;YbWYPomFPwaM+hnvn=HOi^!t>$9=WFI@fqiU!JR!{XY4?ftUk} zCT5uE#kE{X>fQYC*hcr~YwEIJLOGVfO&nQ0f-0`uq zeL~XhnNit(dnP;o=8r4(%-vcRBIXT7A$*S2j#E1=9hQth}>M^OP0<}d*AoO z{r0~oafDMN;8E8St2Z&1Tg;Bjs<8E^ZIKp9aX&m|s>NboOGef5*c|@a>ZO<71lEf; zSi8u0=Fchoy5!DbO;sl~!3A&Daj#uit{y5?Jlo#lpGTVHXM6XRlTJ_bJ0TWcsK+$L zZ2sm_<=D_&dD~CbvvuKN+#BSfd2Nr#^erEC3j*fm z@7XD}>1FDk!aoQ1|KD2kf3wk=()E2a^FPS+Pj-x8+mIUBMSe>jJ@tjO1APJC~@_wuu>JHZbc)_F92n7`s< z*W3pC6Z4J!N4Wo;A!-c@_$hHqGtY%aL_X@VWb1TU{POIvZ^ui&M@o7MOR$MIm~<|l zbWAe5@9cl6%SXl71obNu|7~EkPs-Wl@ig)PV_}gPufNTb8GrXo_x!-V|Ig;6w;SI2 ztTdbdl~v|K|H0S1!pr8!%+`6w@u8ia&&;mrHe2@H)sc($9guN(TNxLK!v+dU!ZTTj{V4NT2%USFAUn$LP=M4HE&=m!gKp36@5x_E0fGds^E%dPir zy|-R`{prg++aE1>cz@=t9MRi;>&y-)8CHatr=72@-I#KQGb77uXPDQ%FKrerM<#uc z%<*8!_$~G?dgX;FQKfM#%?Fx=8Kd^Ma?bS9ydPMr&n~m5{kPO~jip9KncEH>Oy#$G zrWf8}>i5%MC?>4hb?U4s+KU6E5{xP$TQfE)KfCcpFvj)CvzJN7kEA}?GG*iT%6z8j zok96dqEjDVJ}S$o_xR|!fSK!@iVtjz{GcgP$p0bn#~B@iA6+f;bf$H%nB6X&cP!MU zZ|6Cd2b*Q4zP_!^dMEW~)%@fwIZI#P^<;f==ljRIEN-VKS-Xcf_^#a^?)YKmOwHo3 zosB2E1XLR?8mWA{bRgyP`!DQ{bGCgwo9eKp@R+bhM8cMYRR{JNvFg1PV_5uAWL~X> zn1F7fdw=8ANjsm-xvr>Cp1JFa;l?PT+Zji)x~*0rCvAVfYGUK{eTml=Y-W}OW!(f*A>Rw> zWwROXOq=@WltS9?JIbsF{1iJ=Irgy4y*%4jVfw!cmd$HthplE>*|TiH<(CZ(idvyv zK5Ik%?JS?Y_M7iqrO-(MnF$=aE<#D{g$w;3RsKoV-($bfeF^s^KFyU5QPcW3be#AD zR|g1lrJmS-en0Dl+(t}aV9+MhAgT=bW-l`8L~Io&ELnOzPF zKLqs!gio26P4wPBYr2y2Gq%$9EyqKRI;Xd6*m5>@!vROdM;=qN@{3;SSFf$GdS>Np zr6f8jhf90XQiG<34Q?XyH!6k3%l+Z+U{D8thpX-?j-}Qg56AwnUHVDaR#A77-Gn7-ejeWC zM|@rWo_!H8)BaogS*b6pTk~=xSI#TBd`aHX^@fs^AiLBTi+gjwD3{z6^x!R-RN|2J zp89@|poxn@Qh4#dZKb-m3j6=5 zE_>#&dx?Y4TW#IR`-Ky`PB^@I>HBQu?zv?VmwQy5*se=t_D^v%QWezMY|SE;*s~&V zcFDrx4P|f4cd}kETiKT4b0}(=a*LqOkr$s{`bb^RiR>z?sSbVU-!g4ptBF^VsOODM za^?wdIFgTx8m=@-FJXPFb9#!EBkx2t&vh}J`C^ZqmA#)}4hPr^^#DYh3_ z+hOeM$Izun5jQ#X|sZtQ$h&QN(tab@q9 z@QEv)t#VfO;P5|kZ9!B~=~w<7!SAcqR68^L_VRLYyKK#ykfAdxW@WC| z+ss}ouGNAe9#>sAX$ak#Bdxfry5r>2%RT8cLUeMxYO6yfm&v?Rd0Db<@kE!iN9S`Z z_I+^ldCB_cTwab>^tDG9GA1m$)}j5ePkYHut1_L9!R^h;TO-Wv0&j%ud%t=?!OJDP zZkta(KUX2)x!9s_>{FPI9V?!FYx1X~;sRgSwes(ZRB77z_X3B_p}JM(i-l^=^KqHDpY>QaP|FKJq_Pes*;luK3};b8TV$71COHLmjyS(YD?dqWt^fqcS4(o z{FPhBVOz@}zf*$1r0HY8f9Z1z=M z*B)|a>epQ}zCZI@P%NG!U~2VYm+nMcx(^n9QD_Q}29%#&+>%a`zGwtd;@amm$bmj8Q+MU`)D^0V&$ zpEgzRoqN_lq0es?3c9q&F8bKGXI=LC)|BnilT}V^Kf~jAu+f&!jQwh3*(Iy+hpa(H z_WL>BtWZ`G)Rx+Caf!#IU+gpX_qTQImAwC(J?MM>jQjcwA6YK8T70v-WVmv*hkw_R zo$uzQJNt`2+*;QBE2`RlQuxFj>V6Y6AG|y#GO_cFq zYX89dy7Q_BuU_q%;nw%Be!(44okwhoYBukAA?SJC^G2!pT9*j|^?Tl({c&#EU%r#m zRq|%-bZKEa^xxxe+DeuGN}7ANXR06K`N&-OLjSLay+_)z1qy#2F8e&t$+ z|3$8G<-CT*EbM$T1sjZ4)Nj$(KDp3CX4!J_iMtmyckE}oeQ6}O22%l2^W>yEEKvU7Lt&Fpo8o#(%lzYslq z&icrV)w+#e);R>pzHjnrFI-U8X#XJTdhsk3)8NO>heT`I6O^(9exKWVP(_V%|GTP` zH1?(+!W&!@;lAwN^b9xz2^ z_Q+YQlvVu1Hog^G@^-J{%?Sp}-tgW1BF1OioTGeVCmWCaohl|apYh?2$jDFARE{@q`LN^2X}1)mYnz+4 zoC{Su`DdN3$0IhY@?%01cg@hT;11F{xv4<>R+DO(#*L3Q?O&98C)s$4aL?lKY|&0K zxch739?wZ)Ht+uwm}~F7_o<$%WbcMU5e%*Z#o~F2r`UX#mjy|_6zj}r{PI+gvE!$S z(%JdEoyVE9oFpH1&+wc5uzZcjDY3BUd*7yaSoln?^Y#u>Rn*y`r~F86QR>g2$6;J0 z?|;GfcPHp^NBB+0_@WQXlbC6dzVeP(`tD;*dhu1Uee<~&wk0U{?G}AGu+hX z7at3w4E8^{ee9su^<0yeaT6
- {DOWNLOADER_NAME[game.downloader]} + {game.downloader === Downloader.TorBox ? ( +
+ TorBox + TorBox +
+ ) : ( + {DOWNLOADER_NAME[game.downloader]} + )}
diff --git a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx index 191d9ac1..8d650c17 100644 --- a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx @@ -68,11 +68,9 @@ export function DownloadSettingsModal({ return true; }); - /* Gives preference to Real Debrid */ - const selectedDownloader = filteredDownloaders.includes( - Downloader.RealDebrid - ) - ? Downloader.RealDebrid + /* Gives preference to TorBox */ + const selectedDownloader = filteredDownloaders.includes(Downloader.TorBox) + ? Downloader.TorBox : filteredDownloaders[0]; setSelectedDownloader( diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 2d313abb..4ab7443d 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -4,6 +4,7 @@ export enum Downloader { Gofile, PixelDrain, Qiwi, + TorBox, } export enum DownloadSourceStatus { diff --git a/src/shared/index.ts b/src/shared/index.ts index 85868391..e0b09deb 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -92,7 +92,7 @@ export const getDownloadersForUri = (uri: string) => { return [Downloader.RealDebrid]; if (uri.startsWith("magnet:")) { - return [Downloader.Torrent, Downloader.RealDebrid]; + return [Downloader.Torrent, Downloader.TorBox, Downloader.RealDebrid]; } return []; From 91b1c349e797ffe154eab8838129293894d9b9c6 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Wed, 25 Dec 2024 21:59:48 +0000 Subject: [PATCH 02/85] feat: adding torbox integration --- src/main/services/download/torbox.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/services/download/torbox.ts b/src/main/services/download/torbox.ts index 1ef57768..0c0c0574 100644 --- a/src/main/services/download/torbox.ts +++ b/src/main/services/download/torbox.ts @@ -6,7 +6,6 @@ import type { TorBoxAddTorrentRequest, TorBoxRequestLinkRequest, } from "@types"; -import { logger } from "../logger"; export class TorBoxClient { private static instance: AxiosInstance; From c9ae543d3ed2a5581ea00f2256406167cb1f0f11 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Thu, 26 Dec 2024 00:41:57 +0000 Subject: [PATCH 03/85] feat: adding automatic backup on game close --- src/main/events/cloud-save/upload-save-game.ts | 12 ++++++++++-- src/main/services/process-watcher.ts | 5 +++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/main/events/cloud-save/upload-save-game.ts b/src/main/events/cloud-save/upload-save-game.ts index b3a514f5..d39ad177 100644 --- a/src/main/events/cloud-save/upload-save-game.ts +++ b/src/main/events/cloud-save/upload-save-game.ts @@ -40,8 +40,7 @@ const bundleBackup = async ( return tarLocation; }; -const uploadSaveGame = async ( - _event: Electron.IpcMainInvokeEvent, +export const createBackup = async ( objectId: string, shop: GameShop, downloadOptionTitle: string | null @@ -108,4 +107,13 @@ const uploadSaveGame = async ( }); }; +const uploadSaveGame = async ( + _event: Electron.IpcMainInvokeEvent, + objectId: string, + shop: GameShop, + downloadOptionTitle: string | null +) => { + return createBackup(objectId, shop, downloadOptionTitle); +}; + registerEvent("uploadSaveGame", uploadSaveGame); diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts index c6cb7e10..4aa00bb4 100644 --- a/src/main/services/process-watcher.ts +++ b/src/main/services/process-watcher.ts @@ -7,6 +7,7 @@ import { Game } from "@main/entity"; import axios from "axios"; import { exec } from "child_process"; import { ProcessPayload } from "./download/types"; +import { createBackup } from "@main/events/cloud-save/upload-save-game"; const commands = { findWineDir: `lsof -c wine 2>/dev/null | grep '/drive_c/windows$' | head -n 1 | awk '{for(i=9;i<=NF;i++) printf "%s ", $i; print ""}'`, @@ -269,6 +270,10 @@ const onCloseGame = (game: Game) => { gamesPlaytime.delete(game.id); if (game.remoteId) { + // create backup + // todo: check for hydra cloud? + createBackup(game.objectID, game.shop, ""); + updateGamePlaytime( game, performance.now() - gamePlaytime.lastSyncTick, From 6ea1f9034b6c67c6da9cd6c8a0849f2e6902befc Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Wed, 25 Dec 2024 23:15:58 -0300 Subject: [PATCH 04/85] fix: possible fix for pixel drain and torbox cancel download --- python_rpc/http_downloader.py | 5 +++-- python_rpc/main.py | 16 ++++++++-------- python_rpc/torrent_downloader.py | 2 +- src/main/events/auth/sign-out.ts | 4 ---- src/main/services/download/download-manager.ts | 15 +++++++++++---- src/main/services/download/torbox.ts | 13 +++++++------ src/types/torbox.types.ts | 1 + 7 files changed, 31 insertions(+), 25 deletions(-) diff --git a/python_rpc/http_downloader.py b/python_rpc/http_downloader.py index 40e30ccd..71e4b57e 100644 --- a/python_rpc/http_downloader.py +++ b/python_rpc/http_downloader.py @@ -11,11 +11,12 @@ class HttpDownloader: ) ) - def start_download(self, url: str, save_path: str, header: str): + def start_download(self, url: str, save_path: str, header: str, out: str = None): if self.download: self.aria2.resume([self.download]) else: - downloads = self.aria2.add(url, options={"header": header, "dir": save_path}) + downloads = self.aria2.add(url, options={"header": header, "dir": save_path, "out": out}) + self.download = downloads[0] def pause_download(self): diff --git a/python_rpc/main.py b/python_rpc/main.py index 03df83de..7b2c54b9 100644 --- a/python_rpc/main.py +++ b/python_rpc/main.py @@ -28,14 +28,14 @@ if start_download_payload: torrent_downloader = TorrentDownloader(torrent_session) downloads[initial_download['game_id']] = torrent_downloader try: - torrent_downloader.start_download(initial_download['url'], initial_download['save_path'], "") + torrent_downloader.start_download(initial_download['url'], initial_download['save_path']) except Exception as e: print("Error starting torrent download", e) else: http_downloader = HttpDownloader() downloads[initial_download['game_id']] = http_downloader try: - http_downloader.start_download(initial_download['url'], initial_download['save_path'], initial_download.get('header')) + http_downloader.start_download(initial_download['url'], initial_download['save_path'], initial_download.get('header'), initial_download.get("out")) except Exception as e: print("Error starting http download", e) @@ -45,7 +45,7 @@ if start_seeding_payload: torrent_downloader = TorrentDownloader(torrent_session, lt.torrent_flags.upload_mode) downloads[seed['game_id']] = torrent_downloader try: - torrent_downloader.start_download(seed['url'], seed['save_path'], "") + torrent_downloader.start_download(seed['url'], seed['save_path']) except Exception as e: print("Error starting seeding", e) @@ -140,18 +140,18 @@ def action(): if url.startswith('magnet'): if existing_downloader and isinstance(existing_downloader, TorrentDownloader): - existing_downloader.start_download(url, data['save_path'], "") + existing_downloader.start_download(url, data['save_path']) else: torrent_downloader = TorrentDownloader(torrent_session) downloads[game_id] = torrent_downloader - torrent_downloader.start_download(url, data['save_path'], "") + torrent_downloader.start_download(url, data['save_path']) else: if existing_downloader and isinstance(existing_downloader, HttpDownloader): - existing_downloader.start_download(url, data['save_path'], data.get('header')) + existing_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out')) else: http_downloader = HttpDownloader() downloads[game_id] = http_downloader - http_downloader.start_download(url, data['save_path'], data.get('header')) + http_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out')) downloading_game_id = game_id @@ -167,7 +167,7 @@ def action(): elif action == 'resume_seeding': torrent_downloader = TorrentDownloader(torrent_session, lt.torrent_flags.upload_mode) downloads[game_id] = torrent_downloader - torrent_downloader.start_download(data['url'], data['save_path'], "") + torrent_downloader.start_download(data['url'], data['save_path']) elif action == 'pause_seeding': downloader = downloads.get(game_id) if downloader: diff --git a/python_rpc/torrent_downloader.py b/python_rpc/torrent_downloader.py index ca4c2fa8..8de8764e 100644 --- a/python_rpc/torrent_downloader.py +++ b/python_rpc/torrent_downloader.py @@ -102,7 +102,7 @@ class TorrentDownloader: "http://bvarf.tracker.sh:2086/announce", ] - def start_download(self, magnet: str, save_path: str, header: str): + def start_download(self, magnet: str, save_path: str): params = {'url': magnet, 'save_path': save_path, 'trackers': self.trackers, 'flags': self.flags} self.torrent_handle = self.session.add_torrent(params) self.torrent_handle.resume() diff --git a/src/main/events/auth/sign-out.ts b/src/main/events/auth/sign-out.ts index 6b720015..05fbaa86 100644 --- a/src/main/events/auth/sign-out.ts +++ b/src/main/events/auth/sign-out.ts @@ -2,7 +2,6 @@ import { registerEvent } from "../register-event"; import { DownloadManager, HydraApi, gamesPlaytime } from "@main/services"; import { dataSource } from "@main/data-source"; import { DownloadQueue, Game, UserAuth, UserSubscription } from "@main/entity"; -import { PythonRPC } from "@main/services/python-rpc"; const signOut = async (_event: Electron.IpcMainInvokeEvent) => { const databaseOperations = dataSource @@ -27,9 +26,6 @@ const signOut = async (_event: Electron.IpcMainInvokeEvent) => { /* Cancels any ongoing downloads */ DownloadManager.cancelDownload(); - /* Disconnects libtorrent */ - PythonRPC.kill(); - HydraApi.handleSignOut(); await Promise.all([ diff --git a/src/main/services/download/download-manager.ts b/src/main/services/download/download-manager.ts index 1c91c4dc..902a0c4c 100644 --- a/src/main/services/download/download-manager.ts +++ b/src/main/services/download/download-manager.ts @@ -21,6 +21,7 @@ import { RealDebridClient } from "./real-debrid"; import path from "path"; import { logger } from "../logger"; import { TorBoxClient } from "./torbox"; +import axios from "axios"; export class DownloadManager { private static downloadingGameId: number | null = null; @@ -262,11 +263,16 @@ export class DownloadManager { case Downloader.PixelDrain: { const id = game!.uri!.split("/").pop(); + const name = await axios + .get(`https://pixeldrain.com/api/file/${id}/info`) + .then((res) => res.data.name as string); + return { action: "start", game_id: game.id, url: `https://pixeldrain.com/api/file/${id}?download`, save_path: game.downloadPath!, + out: name, }; } case Downloader.Qiwi: { @@ -297,15 +303,16 @@ export class DownloadManager { }; } case Downloader.TorBox: { - const downloadUrl = await TorBoxClient.getDownloadUrl(game.uri!); - console.log(downloadUrl); + const { name, url } = await TorBoxClient.getDownloadInfo(game.uri!); + console.log(url, name); - if (!downloadUrl) return; + if (!url) return; return { action: "start", game_id: game.id, - url: downloadUrl, + url, save_path: game.downloadPath!, + out: name, }; } } diff --git a/src/main/services/download/torbox.ts b/src/main/services/download/torbox.ts index 0c0c0574..7e0c9089 100644 --- a/src/main/services/download/torbox.ts +++ b/src/main/services/download/torbox.ts @@ -74,7 +74,7 @@ export class TorBoxClient { return response.data.data; } - static async getTorrentId(magnetUri: string) { + static async getTorrentIdAndName(magnetUri: string) { const userTorrents = await this.getAllTorrentsFromUser(); const { infoHash } = await parseTorrent(magnetUri); @@ -82,14 +82,15 @@ export class TorBoxClient { (userTorrent) => userTorrent.hash === infoHash ); - if (userTorrent) return userTorrent.id; + if (userTorrent) return { id: userTorrent.id, name: userTorrent.name }; const torrent = await this.addMagnet(magnetUri); - return torrent.torrent_id; + return { id: torrent.torrent_id, name: torrent.name }; } - static async getDownloadUrl(uri: string) { - const id = await this.getTorrentId(uri); - return this.requestLink(id); + static async getDownloadInfo(uri: string) { + const { id, name } = await this.getTorrentIdAndName(uri); + const url = await this.requestLink(id); + return { url, name: `${name}.zip` }; } } diff --git a/src/types/torbox.types.ts b/src/types/torbox.types.ts index a53ccc4c..51e8bd12 100644 --- a/src/types/torbox.types.ts +++ b/src/types/torbox.types.ts @@ -66,6 +66,7 @@ export interface TorBoxAddTorrentRequest { torrent_id: number; name: string; hash: string; + size: number; }; } From abb16e77364b650b7c232aa4c2dd9941402b186d Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sat, 11 Jan 2025 15:19:20 -0300 Subject: [PATCH 05/85] chore: prettier --- src/main/services/hosters/datanodes.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/services/hosters/datanodes.ts b/src/main/services/hosters/datanodes.ts index d77e7d51..ae144418 100644 --- a/src/main/services/hosters/datanodes.ts +++ b/src/main/services/hosters/datanodes.ts @@ -33,7 +33,8 @@ export class DatanodesApi { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36", }, - maxRedirects: 0, validateStatus: (status: number) => status === 302 || status < 400, + maxRedirects: 0, + validateStatus: (status: number) => status === 302 || status < 400, } ); From db2e31b8ccfd5b23f33bac0f3e6f77fd666ccfbd Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sat, 11 Jan 2025 17:52:48 -0300 Subject: [PATCH 06/85] feat: add torbox migration --- src/main/entity/user-preferences.entity.ts | 3 +++ src/main/knex-client.ts | 2 ++ src/main/main.ts | 6 ++++-- ...0250111182229_add_torbox_api_token_column.ts | 17 +++++++++++++++++ src/main/services/download/torbox.ts | 2 +- src/types/index.ts | 1 + 6 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 src/main/migrations/20250111182229_add_torbox_api_token_column.ts diff --git a/src/main/entity/user-preferences.entity.ts b/src/main/entity/user-preferences.entity.ts index a850b42f..109ede5f 100644 --- a/src/main/entity/user-preferences.entity.ts +++ b/src/main/entity/user-preferences.entity.ts @@ -20,6 +20,9 @@ export class UserPreferences { @Column("text", { nullable: true }) realDebridApiToken: string | null; + @Column("text", { nullable: true }) + torBoxApiToken: string | null; + @Column("boolean", { default: false }) downloadNotificationsEnabled: boolean; diff --git a/src/main/knex-client.ts b/src/main/knex-client.ts index 821efc80..c816c7c7 100644 --- a/src/main/knex-client.ts +++ b/src/main/knex-client.ts @@ -17,6 +17,7 @@ import { AddShouldSeedColumn } from "./migrations/20241108200154_add_should_seed import { AddSeedAfterDownloadColumn } from "./migrations/20241108201806_add_seed_after_download"; import { AddHiddenAchievementDescriptionColumn } from "./migrations/20241216140633_add_hidden_achievement_description_column "; import { AddLaunchOptionsColumnToGame } from "./migrations/20241226044022_add_launch_options_column_to_game"; +import { AddTorBoxApiToken } from "./migrations/20250111182229_add_torbox_api_token_column"; export type HydraMigration = Knex.Migration & { name: string }; @@ -39,6 +40,7 @@ class MigrationSource implements Knex.MigrationSource { AddSeedAfterDownloadColumn, AddHiddenAchievementDescriptionColumn, AddLaunchOptionsColumnToGame, + AddTorBoxApiToken, ]); } getMigrationName(migration: HydraMigration): string { diff --git a/src/main/main.ts b/src/main/main.ts index 81916174..d27f0cbd 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -19,10 +19,12 @@ const loadState = async (userPreferences: UserPreferences | null) => { Aria2.spawn(); if (userPreferences?.realDebridApiToken) { - RealDebridClient.authorize(userPreferences?.realDebridApiToken); + RealDebridClient.authorize(userPreferences.realDebridApiToken); } - TorBoxClient.authorize("7371d5ec-52fa-4b87-9052-0c8c96d947cc"); + if (userPreferences?.torBoxApiToken) { + TorBoxClient.authorize(userPreferences?.torBoxApiToken); + } Ludusavi.addManifestToLudusaviConfig(); diff --git a/src/main/migrations/20250111182229_add_torbox_api_token_column.ts b/src/main/migrations/20250111182229_add_torbox_api_token_column.ts new file mode 100644 index 00000000..fc1904fd --- /dev/null +++ b/src/main/migrations/20250111182229_add_torbox_api_token_column.ts @@ -0,0 +1,17 @@ +import type { HydraMigration } from "@main/knex-client"; +import type { Knex } from "knex"; + +export const AddTorBoxApiToken: HydraMigration = { + name: "AddTorBoxApiToken", + up: (knex: Knex) => { + return knex.schema.alterTable("user_preferences", (table) => { + return table.string("torBoxApiToken").nullable(); + }); + }, + + down: async (knex: Knex) => { + return knex.schema.alterTable("user_preferences", (table) => { + return table.dropColumn("torBoxApiToken"); + }); + }, +}; diff --git a/src/main/services/download/torbox.ts b/src/main/services/download/torbox.ts index 7e0c9089..f0af52eb 100644 --- a/src/main/services/download/torbox.ts +++ b/src/main/services/download/torbox.ts @@ -19,7 +19,7 @@ export class TorBoxClient { Authorization: `Bearer ${apiToken}`, }, }); - this.apiToken = "7371d5ec-52fa-4b87-9052-0c8c96d947cc"; + this.apiToken = apiToken; } static async addMagnet(magnet: string) { diff --git a/src/types/index.ts b/src/types/index.ts index 345893a5..92cc566e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -163,6 +163,7 @@ export interface UserPreferences { repackUpdatesNotificationsEnabled: boolean; achievementNotificationsEnabled: boolean; realDebridApiToken: string | null; + torboxApiToken: string | null; preferQuitInsteadOfHiding: boolean; runAtStartup: boolean; startMinimized: boolean; From b1dde446b2cb6507311b5a6d019bb71baa1606bf Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sun, 12 Jan 2025 11:00:20 -0300 Subject: [PATCH 07/85] feat: few adjustments --- src/main/services/download/torbox.ts | 12 ++++++++---- src/types/index.ts | 2 +- src/types/torbox.types.ts | 4 ++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/main/services/download/torbox.ts b/src/main/services/download/torbox.ts index f0af52eb..b0d339dd 100644 --- a/src/main/services/download/torbox.ts +++ b/src/main/services/download/torbox.ts @@ -10,19 +10,19 @@ import type { export class TorBoxClient { private static instance: AxiosInstance; private static readonly baseURL = "https://api.torbox.app/v1/api"; - public static apiToken: string; + private static apiToken: string; static authorize(apiToken: string) { + this.apiToken = apiToken; this.instance = axios.create({ baseURL: this.baseURL, headers: { Authorization: `Bearer ${apiToken}`, }, }); - this.apiToken = apiToken; } - static async addMagnet(magnet: string) { + private static async addMagnet(magnet: string) { const form = new FormData(); form.append("magnet", magnet); @@ -31,6 +31,10 @@ export class TorBoxClient { form ); + if (!response.data.success) { + throw new Error(response.data.detail); + } + return response.data.data; } @@ -74,7 +78,7 @@ export class TorBoxClient { return response.data.data; } - static async getTorrentIdAndName(magnetUri: string) { + private static async getTorrentIdAndName(magnetUri: string) { const userTorrents = await this.getAllTorrentsFromUser(); const { infoHash } = await parseTorrent(magnetUri); diff --git a/src/types/index.ts b/src/types/index.ts index 92cc566e..b6fcbbb4 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -163,7 +163,7 @@ export interface UserPreferences { repackUpdatesNotificationsEnabled: boolean; achievementNotificationsEnabled: boolean; realDebridApiToken: string | null; - torboxApiToken: string | null; + torBoxApiToken: string | null; preferQuitInsteadOfHiding: boolean; runAtStartup: boolean; startMinimized: boolean; diff --git a/src/types/torbox.types.ts b/src/types/torbox.types.ts index 51e8bd12..ee72600a 100644 --- a/src/types/torbox.types.ts +++ b/src/types/torbox.types.ts @@ -54,14 +54,14 @@ export interface TorBoxTorrentInfo { export interface TorBoxTorrentInfoRequest { success: boolean; detail: string; - error: string; + error: string | null; data: TorBoxTorrentInfo[]; } export interface TorBoxAddTorrentRequest { success: boolean; detail: string; - error: string; + error: string | null; data: { torrent_id: number; name: string; From 87613023842e0e412210022fc5d499f250b06dcf Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sun, 12 Jan 2025 11:01:18 -0300 Subject: [PATCH 08/85] feat: debrid token input component --- src/locales/en/translation.json | 3 +- src/locales/pt-BR/translation.json | 3 +- .../pages/settings/settings-debrid-input.tsx | 186 ++++++++++++++++++ ...l-debrid.css.ts => settings-debrid.css.ts} | 0 ...gs-real-debrid.tsx => settings-debrid.tsx} | 66 ++++++- src/renderer/src/pages/settings/settings.tsx | 6 +- src/types/index.ts | 2 + 7 files changed, 252 insertions(+), 14 deletions(-) create mode 100644 src/renderer/src/pages/settings/settings-debrid-input.tsx rename src/renderer/src/pages/settings/{settings-real-debrid.css.ts => settings-debrid.css.ts} (100%) rename src/renderer/src/pages/settings/{settings-real-debrid.tsx => settings-debrid.tsx} (65%) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 4e3dcb37..233d04e4 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -280,7 +280,8 @@ "launch_minimized": "Launch Hydra minimized", "disable_nsfw_alert": "Disable NSFW alert", "seed_after_download_complete": "Seed after download complete", - "show_hidden_achievement_description": "Show hidden achievements description before unlocking them" + "show_hidden_achievement_description": "Show hidden achievements description before unlocking them", + "debrid_services": "Debrid Services" }, "notifications": { "download_complete": "Download complete", diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index 2a80084f..453aff7c 100644 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -268,7 +268,8 @@ "launch_minimized": "Iniciar o Hydra minimizado", "disable_nsfw_alert": "Desativar alerta de conteúdo inapropriado", "seed_after_download_complete": "Semear após a conclusão do download", - "show_hidden_achievement_description": "Mostrar descrição de conquistas ocultas antes de debloqueá-las" + "show_hidden_achievement_description": "Mostrar descrição de conquistas ocultas antes de debloqueá-las", + "debrid_services": "Serviços Debrid" }, "notifications": { "download_complete": "Download concluído", diff --git a/src/renderer/src/pages/settings/settings-debrid-input.tsx b/src/renderer/src/pages/settings/settings-debrid-input.tsx new file mode 100644 index 00000000..5223e378 --- /dev/null +++ b/src/renderer/src/pages/settings/settings-debrid-input.tsx @@ -0,0 +1,186 @@ +import { useContext, useEffect, useMemo, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { Button, CheckboxField, Link, TextField } from "@renderer/components"; +import * as styles from "./settings-debrid.css"; +import { useAppSelector, useToast } from "@renderer/hooks"; +import { SPACING_UNIT } from "@renderer/theme.css"; +import { settingsContext } from "@renderer/context"; +import { DebridServices } from "@types"; + +const REAL_DEBRID_API_TOKEN_URL = "https://real-debrid.com/apitoken"; +const TORBOX_API_TOKEN_URL = "https://torbox.app/settings"; + +interface SettingsDebridForm { + useRealDebrid: boolean; + realDebridApiToken: string | null; + useTorBox: boolean; + torBoxApiToken: string | null; +} + +export interface SettingsDebridProps { + service: DebridServices; + form: SettingsDebridForm; + setForm: (SettingsDebridForm) => void; +} + +export function SettingsDebridInput({ + service, + form, + setForm, +}: SettingsDebridProps) { + const userPreferences = useAppSelector( + (state) => state.userPreferences.value + ); + + const { updateUserPreferences } = useContext(settingsContext); + + const [isLoading, setIsLoading] = useState(false); + + const { showSuccessToast, showErrorToast } = useToast(); + + const { t } = useTranslation("settings"); + + useEffect(() => { + if (userPreferences) { + setForm({ + useRealDebrid: Boolean(userPreferences.realDebridApiToken), + realDebridApiToken: userPreferences.realDebridApiToken ?? null, + useTorBox: Boolean(userPreferences.torBoxApiToken), + torBoxApiToken: userPreferences.torBoxApiToken ?? null, + }); + } + }, [userPreferences]); + + const handleFormSubmit: React.FormEventHandler = async ( + event + ) => { + setIsLoading(true); + event.preventDefault(); + + try { + if (form.useRealDebrid) { + const user = await window.electron.authenticateRealDebrid( + form.realDebridApiToken! + ); + + if (user.type === "free") { + showErrorToast( + t("real_debrid_free_account_error", { username: user.username }) + ); + + return; + } else { + showSuccessToast( + t("real_debrid_linked_message", { username: user.username }) + ); + } + } else { + showSuccessToast(t("changes_saved")); + } + + updateUserPreferences({ + realDebridApiToken: form.useRealDebrid ? form.realDebridApiToken : null, + }); + } catch (err) { + showErrorToast(t("real_debrid_invalid_token")); + } finally { + setIsLoading(false); + } + }; + + const useDebridService = useMemo(() => { + if (service === "RealDebrid") { + return form.useRealDebrid; + } + + if (service === "TorBox") { + return form.useTorBox; + } + + return false; + }, [form, service]); + + const debridApiToken = useMemo(() => { + if (service === "RealDebrid") { + return form.realDebridApiToken; + } + + if (service === "TorBox") { + return form.torBoxApiToken; + } + + return null; + }, [form, service]); + + const onChangeCheckbox = () => { + if (service === "RealDebrid") { + setForm((prev) => ({ + ...prev, + useRealDebrid: !form.useRealDebrid, + })); + } + + if (service === "TorBox") { + setForm((prev) => ({ + ...prev, + useTorBox: !form.useTorBox, + })); + } + }; + + const onChangeInput = (event: React.ChangeEvent) => { + if (service === "RealDebrid") { + setForm((prev) => ({ + ...prev, + realDebridApiToken: event.target.value, + })); + } + + if (service === "TorBox") { + setForm((prev) => ({ + ...prev, + torBoxApiToken: event.target.value, + })); + } + }; + + const isButtonDisabled = + (form.useRealDebrid && !form.realDebridApiToken) || isLoading; + + return ( +
+ + + {useDebridService && ( + + {t("save")} + + } + hint={ + + + + } + /> + )} + + ); +} diff --git a/src/renderer/src/pages/settings/settings-real-debrid.css.ts b/src/renderer/src/pages/settings/settings-debrid.css.ts similarity index 100% rename from src/renderer/src/pages/settings/settings-real-debrid.css.ts rename to src/renderer/src/pages/settings/settings-debrid.css.ts diff --git a/src/renderer/src/pages/settings/settings-real-debrid.tsx b/src/renderer/src/pages/settings/settings-debrid.tsx similarity index 65% rename from src/renderer/src/pages/settings/settings-real-debrid.tsx rename to src/renderer/src/pages/settings/settings-debrid.tsx index 35804664..9d63e68a 100644 --- a/src/renderer/src/pages/settings/settings-real-debrid.tsx +++ b/src/renderer/src/pages/settings/settings-debrid.tsx @@ -2,7 +2,7 @@ import { useContext, useEffect, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import { Button, CheckboxField, Link, TextField } from "@renderer/components"; -import * as styles from "./settings-real-debrid.css"; +import * as styles from "./settings-debrid.css"; import { useAppSelector, useToast } from "@renderer/hooks"; @@ -10,8 +10,9 @@ import { SPACING_UNIT } from "@renderer/theme.css"; import { settingsContext } from "@renderer/context"; const REAL_DEBRID_API_TOKEN_URL = "https://real-debrid.com/apitoken"; +const TORBOX_API_TOKEN_URL = "https://torbox.app/settings"; -export function SettingsRealDebrid() { +export function SettingsDebrid() { const userPreferences = useAppSelector( (state) => state.userPreferences.value ); @@ -22,6 +23,8 @@ export function SettingsRealDebrid() { const [form, setForm] = useState({ useRealDebrid: false, realDebridApiToken: null as string | null, + useTorBox: false, + torBoxApiToken: null as string | null, }); const { showSuccessToast, showErrorToast } = useToast(); @@ -33,6 +36,8 @@ export function SettingsRealDebrid() { setForm({ useRealDebrid: Boolean(userPreferences.realDebridApiToken), realDebridApiToken: userPreferences.realDebridApiToken ?? null, + useTorBox: Boolean(userPreferences.torBoxApiToken), + torBoxApiToken: userPreferences.torBoxApiToken ?? null, }); } }, [userPreferences]); @@ -102,6 +107,17 @@ export function SettingsRealDebrid() { } placeholder="API Token" containerProps={{ style: { marginTop: `${SPACING_UNIT}px` } }} + rightContent={ + + } hint={ @@ -110,13 +126,45 @@ export function SettingsRealDebrid() { /> )} - + + setForm((prev) => ({ + ...prev, + useTorBox: !form.useTorBox, + })) + } + /> + + {form.useTorBox && ( + + setForm({ ...form, torBoxApiToken: event.target.value }) + } + placeholder="API Token" + containerProps={{ style: { marginTop: `${SPACING_UNIT}px` } }} + rightContent={ + + } + hint={ + + + + } + /> + )} ); } diff --git a/src/renderer/src/pages/settings/settings.tsx b/src/renderer/src/pages/settings/settings.tsx index dffdfbae..00ceebd7 100644 --- a/src/renderer/src/pages/settings/settings.tsx +++ b/src/renderer/src/pages/settings/settings.tsx @@ -2,7 +2,7 @@ import { Button } from "@renderer/components"; import * as styles from "./settings.css"; import { useTranslation } from "react-i18next"; -import { SettingsRealDebrid } from "./settings-real-debrid"; +import { SettingsDebrid } from "./settings-debrid"; import { SettingsGeneral } from "./settings-general"; import { SettingsBehavior } from "./settings-behavior"; @@ -25,7 +25,7 @@ export default function Settings() { t("general"), t("behavior"), t("download_sources"), - "Real-Debrid", + t("debrid_services"), ]; if (userDetails) return [...categories, t("privacy")]; @@ -50,7 +50,7 @@ export default function Settings() { } if (currentCategoryIndex === 3) { - return ; + return ; } return ; diff --git a/src/types/index.ts b/src/types/index.ts index b6fcbbb4..da5deea4 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -19,6 +19,8 @@ export type HydraCloudFeature = | "backup" | "achievements-points"; +export type DebridServices = "RealDebrid" | "TorBox"; + export interface GameRepack { id: number; title: string; From b4014535e8987c95e7d9183eee77b95f8bf30c57 Mon Sep 17 00:00:00 2001 From: Shisuys Date: Mon, 13 Jan 2025 10:23:50 -0300 Subject: [PATCH 09/85] change pixeldrain url to gamedrivers servers --- src/main/services/download/download-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/services/download/download-manager.ts b/src/main/services/download/download-manager.ts index 134a74e6..8fda319e 100644 --- a/src/main/services/download/download-manager.ts +++ b/src/main/services/download/download-manager.ts @@ -263,7 +263,7 @@ export class DownloadManager { return { action: "start", game_id: game.id, - url: `https://pixeldrain.com/api/file/${id}?download`, + url: `https://cdn.pd5-gamedriveorg.workers.dev/api/file/${id}`, save_path: game.downloadPath!, }; } From 2c5fb8a0379c1515a3f2b4874ea5ec23c7bc8344 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Wed, 15 Jan 2025 16:58:59 +0000 Subject: [PATCH 10/85] feat: adding initial leveldb configuration --- electron.vite.config.ts | 7 ++ package.json | 1 + src/main/constants.ts | 7 +- src/main/data-source.ts | 4 -- src/main/entity/index.ts | 2 - src/main/entity/user-auth.entity.ts | 45 ------------ src/main/entity/user-subscription.entity.ts | 42 ------------ src/main/events/auth/get-session-hash.ts | 13 +++- src/main/events/auth/sign-out.ts | 21 +++--- src/main/events/misc/open-checkout.ts | 14 ++-- src/main/events/user/get-user-friends.ts | 11 +-- src/main/repository.ts | 7 -- src/main/services/hydra-api.ts | 71 ++++++++++++------- src/main/services/index.ts | 1 + src/main/services/user/get-user-data.ts | 64 +++++++---------- src/renderer/src/components/modal/modal.tsx | 1 + src/types/index.ts | 11 +-- yarn.lock | 76 +++++++++++++++++++++ 18 files changed, 202 insertions(+), 196 deletions(-) delete mode 100644 src/main/entity/user-auth.entity.ts delete mode 100644 src/main/entity/user-subscription.entity.ts diff --git a/electron.vite.config.ts b/electron.vite.config.ts index cd08b6d4..2b7048c4 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -38,6 +38,13 @@ export default defineConfig(({ mode }) => { build: { sourcemap: true, }, + css: { + preprocessorOptions: { + scss: { + api: "modern", + }, + }, + }, resolve: { alias: { "@renderer": resolve("src/renderer/src"), diff --git a/package.json b/package.json index 2895f20c..4630ad41 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "jsdom": "^24.0.0", "jsonwebtoken": "^9.0.2", "knex": "^3.1.0", + "level": "^9.0.0", "lodash-es": "^4.17.21", "parse-torrent": "^11.0.17", "piscina": "^4.7.0", diff --git a/src/main/constants.ts b/src/main/constants.ts index b98b5935..f9d9c3e2 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -7,13 +7,18 @@ export const defaultDownloadsPath = app.getPath("downloads"); export const isStaging = import.meta.env.MAIN_VITE_API_URL.includes("staging"); +export const levelDatabasePath = path.join( + app.getPath("userData"), + `hydra-db${isStaging ? "-staging" : ""}` +); + export const databaseDirectory = path.join(app.getPath("appData"), "hydra"); export const databasePath = path.join( databaseDirectory, isStaging ? "hydra_test.db" : "hydra.db" ); -export const logsPath = path.join(app.getPath("appData"), "hydra", "logs"); +export const logsPath = path.join(app.getPath("userData"), "hydra", "logs"); export const seedsPath = app.isPackaged ? path.join(process.resourcesPath, "seeds") diff --git a/src/main/data-source.ts b/src/main/data-source.ts index 51c8522e..05fdb04d 100644 --- a/src/main/data-source.ts +++ b/src/main/data-source.ts @@ -4,9 +4,7 @@ import { Game, GameShopCache, UserPreferences, - UserAuth, GameAchievement, - UserSubscription, } from "@main/entity"; import { databasePath } from "./constants"; @@ -15,9 +13,7 @@ export const dataSource = new DataSource({ type: "better-sqlite3", entities: [ Game, - UserAuth, UserPreferences, - UserSubscription, GameShopCache, DownloadQueue, GameAchievement, diff --git a/src/main/entity/index.ts b/src/main/entity/index.ts index 1625ac8a..ab0ebff9 100644 --- a/src/main/entity/index.ts +++ b/src/main/entity/index.ts @@ -1,7 +1,5 @@ export * from "./game.entity"; -export * from "./user-auth.entity"; export * from "./user-preferences.entity"; -export * from "./user-subscription.entity"; export * from "./game-shop-cache.entity"; export * from "./game.entity"; export * from "./game-achievements.entity"; diff --git a/src/main/entity/user-auth.entity.ts b/src/main/entity/user-auth.entity.ts deleted file mode 100644 index f34e23ec..00000000 --- a/src/main/entity/user-auth.entity.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - OneToOne, -} from "typeorm"; -import { UserSubscription } from "./user-subscription.entity"; - -@Entity("user_auth") -export class UserAuth { - @PrimaryGeneratedColumn() - id: number; - - @Column("text", { default: "" }) - userId: string; - - @Column("text", { default: "" }) - displayName: string; - - @Column("text", { nullable: true }) - profileImageUrl: string | null; - - @Column("text", { nullable: true }) - backgroundImageUrl: string | null; - - @Column("text", { default: "" }) - accessToken: string; - - @Column("text", { default: "" }) - refreshToken: string; - - @Column("int", { default: 0 }) - tokenExpirationTimestamp: number; - - @OneToOne("UserSubscription", "user") - subscription: UserSubscription | null; - - @CreateDateColumn() - createdAt: Date; - - @UpdateDateColumn() - updatedAt: Date; -} diff --git a/src/main/entity/user-subscription.entity.ts b/src/main/entity/user-subscription.entity.ts deleted file mode 100644 index e74ada48..00000000 --- a/src/main/entity/user-subscription.entity.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { SubscriptionStatus } from "@types"; -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - OneToOne, - JoinColumn, -} from "typeorm"; -import { UserAuth } from "./user-auth.entity"; - -@Entity("user_subscription") -export class UserSubscription { - @PrimaryGeneratedColumn() - id: number; - - @Column("text", { default: "" }) - subscriptionId: string; - - @OneToOne("UserAuth", "subscription") - @JoinColumn() - user: UserAuth; - - @Column("text", { default: "" }) - status: SubscriptionStatus; - - @Column("text", { default: "" }) - planId: string; - - @Column("text", { default: "" }) - planName: string; - - @Column("datetime", { nullable: true }) - expiresAt: Date | null; - - @CreateDateColumn() - createdAt: Date; - - @UpdateDateColumn() - updatedAt: Date; -} diff --git a/src/main/events/auth/get-session-hash.ts b/src/main/events/auth/get-session-hash.ts index c9dd39cc..293fb62e 100644 --- a/src/main/events/auth/get-session-hash.ts +++ b/src/main/events/auth/get-session-hash.ts @@ -1,13 +1,20 @@ import jwt from "jsonwebtoken"; -import { userAuthRepository } from "@main/repository"; import { registerEvent } from "../register-event"; +import { db } from "@main/level"; +import type { Auth } from "@types"; +import { levelKeys } from "@main/level/sublevels/keys"; +import { Crypto } from "@main/services"; const getSessionHash = async (_event: Electron.IpcMainInvokeEvent) => { - const auth = await userAuthRepository.findOne({ where: { id: 1 } }); + const auth = await db.get(levelKeys.auth, { + valueEncoding: "json", + }); if (!auth) return null; - const payload = jwt.decode(auth.accessToken) as jwt.JwtPayload; + const payload = jwt.decode( + Crypto.decrypt(auth.accessToken) + ) as jwt.JwtPayload; if (!payload) return null; diff --git a/src/main/events/auth/sign-out.ts b/src/main/events/auth/sign-out.ts index 6b720015..1fb3a054 100644 --- a/src/main/events/auth/sign-out.ts +++ b/src/main/events/auth/sign-out.ts @@ -1,8 +1,10 @@ import { registerEvent } from "../register-event"; import { DownloadManager, HydraApi, gamesPlaytime } from "@main/services"; import { dataSource } from "@main/data-source"; -import { DownloadQueue, Game, UserAuth, UserSubscription } from "@main/entity"; +import { DownloadQueue, Game } from "@main/entity"; import { PythonRPC } from "@main/services/python-rpc"; +import { db } from "@main/level"; +import { levelKeys } from "@main/level/sublevels/keys"; const signOut = async (_event: Electron.IpcMainInvokeEvent) => { const databaseOperations = dataSource @@ -11,13 +13,16 @@ const signOut = async (_event: Electron.IpcMainInvokeEvent) => { await transactionalEntityManager.getRepository(Game).delete({}); - await transactionalEntityManager - .getRepository(UserAuth) - .delete({ id: 1 }); - - await transactionalEntityManager - .getRepository(UserSubscription) - .delete({ id: 1 }); + await db.batch([ + { + type: "del", + key: levelKeys.auth, + }, + { + type: "del", + key: levelKeys.user, + }, + ]); }) .then(() => { /* Removes all games being played */ diff --git a/src/main/events/misc/open-checkout.ts b/src/main/events/misc/open-checkout.ts index ba48f03b..76e7fe09 100644 --- a/src/main/events/misc/open-checkout.ts +++ b/src/main/events/misc/open-checkout.ts @@ -1,17 +1,21 @@ import { shell } from "electron"; import { registerEvent } from "../register-event"; -import { userAuthRepository } from "@main/repository"; -import { HydraApi } from "@main/services"; +import { Crypto, HydraApi } from "@main/services"; +import { db } from "@main/level"; +import type { Auth } from "@types"; +import { levelKeys } from "@main/level/sublevels/keys"; const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => { - const userAuth = await userAuthRepository.findOne({ where: { id: 1 } }); + const auth = await db.get(levelKeys.auth, { + valueEncoding: "json", + }); - if (!userAuth) { + if (!auth) { return; } const paymentToken = await HydraApi.post("/auth/payment", { - refreshToken: userAuth.refreshToken, + refreshToken: Crypto.decrypt(auth.refreshToken), }).then((response) => response.accessToken); const params = new URLSearchParams({ diff --git a/src/main/events/user/get-user-friends.ts b/src/main/events/user/get-user-friends.ts index 9a6f156c..7c308506 100644 --- a/src/main/events/user/get-user-friends.ts +++ b/src/main/events/user/get-user-friends.ts @@ -1,16 +1,19 @@ -import { userAuthRepository } from "@main/repository"; +import { db } from "@main/level"; import { registerEvent } from "../register-event"; import { HydraApi } from "@main/services"; -import type { UserFriends } from "@types"; +import type { User, UserFriends } from "@types"; +import { levelKeys } from "@main/level/sublevels/keys"; export const getUserFriends = async ( userId: string, take: number, skip: number ): Promise => { - const loggedUser = await userAuthRepository.findOne({ where: { id: 1 } }); + const user = await db.get(levelKeys.user, { + valueEncoding: "json", + }); - if (loggedUser?.userId === userId) { + if (user?.id === userId) { return HydraApi.get(`/profile/friends`, { take, skip }); } diff --git a/src/main/repository.ts b/src/main/repository.ts index e0c4204e..ef120f7e 100644 --- a/src/main/repository.ts +++ b/src/main/repository.ts @@ -4,9 +4,7 @@ import { Game, GameShopCache, UserPreferences, - UserAuth, GameAchievement, - UserSubscription, } from "@main/entity"; export const gameRepository = dataSource.getRepository(Game); @@ -18,10 +16,5 @@ export const gameShopCacheRepository = dataSource.getRepository(GameShopCache); export const downloadQueueRepository = dataSource.getRepository(DownloadQueue); -export const userAuthRepository = dataSource.getRepository(UserAuth); - -export const userSubscriptionRepository = - dataSource.getRepository(UserSubscription); - export const gameAchievementRepository = dataSource.getRepository(GameAchievement); diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index 63dd9b16..6cf9a8af 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -1,7 +1,3 @@ -import { - userAuthRepository, - userSubscriptionRepository, -} from "@main/repository"; import axios, { AxiosError, AxiosInstance } from "axios"; import { WindowManager } from "./window-manager"; import url from "url"; @@ -13,6 +9,10 @@ import { omit } from "lodash-es"; import { appVersion } from "@main/constants"; import { getUserData } from "./user/get-user-data"; import { isFuture, isToday } from "date-fns"; +import { db } from "@main/level"; +import { levelKeys } from "@main/level/sublevels/keys"; +import type { Auth, User } from "@types"; +import { Crypto } from "./crypto"; interface HydraApiOptions { needsAuth?: boolean; @@ -77,14 +77,14 @@ export class HydraApi { tokenExpirationTimestamp ); - await userAuthRepository.upsert( + db.put( + levelKeys.auth, { - id: 1, - accessToken, + accessToken: Crypto.encrypt(accessToken), + refreshToken: Crypto.encrypt(refreshToken), tokenExpirationTimestamp, - refreshToken, }, - ["id"] + { valueEncoding: "json" } ); await getUserData().then((userDetails) => { @@ -186,17 +186,23 @@ export class HydraApi { ); } - const userAuth = await userAuthRepository.findOne({ - where: { id: 1 }, - relations: { subscription: true }, + const result = await db.getMany([levelKeys.auth, levelKeys.user], { + valueEncoding: "json", }); + const userAuth = result.at(0) as Auth | undefined; + const user = result.at(1) as User | undefined; + this.userAuth = { - authToken: userAuth?.accessToken ?? "", - refreshToken: userAuth?.refreshToken ?? "", + authToken: userAuth?.accessToken + ? Crypto.decrypt(userAuth.accessToken) + : "", + refreshToken: userAuth?.refreshToken + ? Crypto.decrypt(userAuth.refreshToken) + : "", expirationTimestamp: userAuth?.tokenExpirationTimestamp ?? 0, - subscription: userAuth?.subscription - ? { expiresAt: userAuth.subscription?.expiresAt } + subscription: user?.subscription + ? { expiresAt: user.subscription?.expiresAt } : null, }; @@ -239,14 +245,19 @@ export class HydraApi { this.userAuth.expirationTimestamp ); - userAuthRepository.upsert( - { - id: 1, - accessToken, - tokenExpirationTimestamp, - }, - ["id"] - ); + await db + .get(levelKeys.auth, { valueEncoding: "json" }) + .then((auth) => { + return db.put( + levelKeys.auth, + { + ...auth, + accessToken: Crypto.encrypt(accessToken), + tokenExpirationTimestamp, + }, + { valueEncoding: "json" } + ); + }); } catch (err) { this.handleUnauthorizedError(err); } @@ -276,8 +287,16 @@ export class HydraApi { subscription: null, }; - userAuthRepository.delete({ id: 1 }); - userSubscriptionRepository.delete({ id: 1 }); + db.batch([ + { + type: "del", + key: levelKeys.auth, + }, + { + type: "del", + key: levelKeys.user, + }, + ]); this.sendSignOutEvent(); } diff --git a/src/main/services/index.ts b/src/main/services/index.ts index 5aaf5322..d2034f15 100644 --- a/src/main/services/index.ts +++ b/src/main/services/index.ts @@ -1,3 +1,4 @@ +export * from "./crypto"; export * from "./logger"; export * from "./steam"; export * from "./steam-250"; diff --git a/src/main/services/user/get-user-data.ts b/src/main/services/user/get-user-data.ts index 7e924454..e6cf1c71 100644 --- a/src/main/services/user/get-user-data.ts +++ b/src/main/services/user/get-user-data.ts @@ -1,43 +1,30 @@ -import type { ProfileVisibility, UserDetails } from "@types"; +import { User, type ProfileVisibility, type UserDetails } from "@types"; import { HydraApi } from "../hydra-api"; -import { - userAuthRepository, - userSubscriptionRepository, -} from "@main/repository"; import { UserNotLoggedInError } from "@shared"; import { logger } from "../logger"; +import { db } from "@main/level"; +import { levelKeys } from "@main/level/sublevels/keys"; -export const getUserData = () => { +export const getUserData = async () => { return HydraApi.get(`/profile/me`) .then(async (me) => { - userAuthRepository.upsert( - { - id: 1, - displayName: me.displayName, - profileImageUrl: me.profileImageUrl, - backgroundImageUrl: me.backgroundImageUrl, - userId: me.id, - }, - ["id"] + db.get(levelKeys.user, { valueEncoding: "json" }).then( + (user) => { + return db.put( + levelKeys.user, + { + ...user, + id: me.id, + displayName: me.displayName, + profileImageUrl: me.profileImageUrl, + backgroundImageUrl: me.backgroundImageUrl, + subscription: me.subscription, + }, + { valueEncoding: "json" } + ); + } ); - if (me.subscription) { - await userSubscriptionRepository.upsert( - { - id: 1, - subscriptionId: me.subscription?.id || "", - status: me.subscription?.status || "", - planId: me.subscription?.plan.id || "", - planName: me.subscription?.plan.name || "", - expiresAt: me.subscription?.expiresAt || null, - user: { id: 1 }, - }, - ["id"] - ); - } else { - await userSubscriptionRepository.delete({ id: 1 }); - } - return me; }) .catch(async (err) => { @@ -46,15 +33,14 @@ export const getUserData = () => { return null; } logger.error("Failed to get logged user"); - const loggedUser = await userAuthRepository.findOne({ - where: { id: 1 }, - relations: { subscription: true }, + + const loggedUser = await db.get(levelKeys.user, { + valueEncoding: "json", }); if (loggedUser) { return { ...loggedUser, - id: loggedUser.userId, username: "", bio: "", email: null, @@ -64,11 +50,11 @@ export const getUserData = () => { }, subscription: loggedUser.subscription ? { - id: loggedUser.subscription.subscriptionId, + id: loggedUser.subscription.id, status: loggedUser.subscription.status, plan: { - id: loggedUser.subscription.planId, - name: loggedUser.subscription.planName, + id: loggedUser.subscription.plan.id, + name: loggedUser.subscription.plan.name, }, expiresAt: loggedUser.subscription.expiresAt, } diff --git a/src/renderer/src/components/modal/modal.tsx b/src/renderer/src/components/modal/modal.tsx index d8d0554d..af09ef38 100644 --- a/src/renderer/src/components/modal/modal.tsx +++ b/src/renderer/src/components/modal/modal.tsx @@ -52,6 +52,7 @@ export function Modal({ ) ) return false; + const openModals = document.querySelectorAll("[role=dialog]"); return ( diff --git a/src/types/index.ts b/src/types/index.ts index 345893a5..bae42702 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -248,16 +248,6 @@ export interface UserProfileCurrentGame extends Omit { export type ProfileVisibility = "PUBLIC" | "PRIVATE" | "FRIENDS"; -export type SubscriptionStatus = "active" | "pending" | "cancelled"; - -export interface Subscription { - id: string; - status: SubscriptionStatus; - plan: { id: string; name: string }; - expiresAt: string | null; - paymentMethod: "pix" | "paypal"; -} - export interface UserDetails { id: string; username: string; @@ -421,3 +411,4 @@ export * from "./real-debrid.types"; export * from "./ludusavi.types"; export * from "./how-long-to-beat.types"; export * from "./torbox.types"; +export * from "./level.types"; diff --git a/yarn.lock b/yarn.lock index 69ee75d8..4e58584e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3699,6 +3699,18 @@ abort-controller@^3.0.0: dependencies: event-target-shim "^5.0.0" +abstract-level@^2.0.0, abstract-level@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/abstract-level/-/abstract-level-2.0.2.tgz#8d965e731afb42a72f163874410c1687fb2e4bdb" + integrity sha512-pPJixmXk/kTKLB2sSue7o4Uj6TlLD2XfaP2gWZomHVCC6cuUGX/VslQqKG1yZHfXwBb/3lS6oSTMPGzh1P1iig== + dependencies: + buffer "^6.0.3" + is-buffer "^2.0.5" + level-supports "^6.0.0" + level-transcoder "^1.0.1" + maybe-combine-errors "^1.0.0" + module-error "^1.0.1" + acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" @@ -4169,6 +4181,13 @@ braces@^3.0.3, braces@~3.0.2: dependencies: fill-range "^7.1.1" +browser-level@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/browser-level/-/browser-level-2.0.0.tgz#cc63eb1322e67c44489d7fbdda5c30a2db7b59da" + integrity sha512-RuYSCHG/jwFCrK+KWA3dLSUNLKHEgIYhO5ORPjJMjCt7T3e+RzpIDmYKWRHxq2pfKGXjlRuEff7y7RESAAgzew== + dependencies: + abstract-level "^2.0.1" + browserslist@^4.22.2, browserslist@^4.23.1: version "4.24.0" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.0.tgz#a1325fe4bc80b64fda169629fc01b3d6cecd38d4" @@ -4421,6 +4440,16 @@ ci-info@^3.2.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== +classic-level@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/classic-level/-/classic-level-2.0.0.tgz#6fd9ca686bbcd645e35caaf403c3f3a56495d11b" + integrity sha512-ftiMvKgCQK+OppXcvMieDoYlYLYWhScK6yZRFBrrlHQRbm4k6Gr+yDgu/wt3V0k1/jtNbuiXAsRmuAFcD0Tx5Q== + dependencies: + abstract-level "^2.0.0" + module-error "^1.0.1" + napi-macros "^2.2.2" + node-gyp-build "^4.3.0" + classnames@^2.2.1, classnames@^2.2.6, classnames@^2.5.1: version "2.5.1" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" @@ -6459,6 +6488,11 @@ is-boolean-object@^1.2.0: call-bound "^1.0.2" has-tostringtag "^1.0.2" +is-buffer@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" + integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== + is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" @@ -6958,6 +6992,28 @@ lazy-val@^1.0.5: resolved "https://registry.yarnpkg.com/lazy-val/-/lazy-val-1.0.5.tgz#6cf3b9f5bc31cee7ee3e369c0832b7583dcd923d" integrity sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q== +level-supports@^6.0.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/level-supports/-/level-supports-6.2.0.tgz#e78b228973a24acdc5199c5f51e244e70f26c611" + integrity sha512-QNxVXP0IRnBmMsJIh+sb2kwNCYcKciQZJEt+L1hPCHrKNELllXhvrlClVHXBYZVT+a7aTSM6StgNXdAldoab3w== + +level-transcoder@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/level-transcoder/-/level-transcoder-1.0.1.tgz#f8cef5990c4f1283d4c86d949e73631b0bc8ba9c" + integrity sha512-t7bFwFtsQeD8cl8NIoQ2iwxA0CL/9IFw7/9gAjOonH0PWTTiRfY7Hq+Ejbsxh86tXobDQ6IOiddjNYIfOBs06w== + dependencies: + buffer "^6.0.3" + module-error "^1.0.1" + +level@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/level/-/level-9.0.0.tgz#880aa9d341a5411e36bed77f4fa233f425b492a8" + integrity sha512-n+mVuf63mUEkd8NUx7gwxY+QF5vtkibv6fXTGUgtHWLPDaA5/XZjLcI/Q1nQ8k6OttHT6Ezt+7nSEXsRUfHtOQ== + dependencies: + abstract-level "^2.0.1" + browser-level "^2.0.0" + classic-level "^2.0.0" + levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -7198,6 +7254,11 @@ math-intrinsics@^1.0.0: resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.0.0.tgz#4e04bf87c85aa51e90d078dac2252b4eb5260817" integrity sha512-4MqMiKP90ybymYvsut0CH2g4XWbfLtmlCkXmtmdcDCxNB+mQcu1w/1+L/VD7vi/PSv7X2JYV7SCcR+jiPXnQtA== +maybe-combine-errors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/maybe-combine-errors/-/maybe-combine-errors-1.0.0.tgz#e9592832e61fc47643a92cff3c1f33e27211e5be" + integrity sha512-eefp6IduNPT6fVdwPp+1NgD0PML1NU5P6j1Mj5nz1nidX8/sWY7119WL8vTAHgqfsY74TzW0w1XPgdYEKkGZ5A== + media-query-parser@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/media-query-parser/-/media-query-parser-2.0.2.tgz#ff79e56cee92615a304a1c2fa4f2bd056c0a1d29" @@ -7414,6 +7475,11 @@ modern-ahocorasick@^1.0.0: resolved "https://registry.yarnpkg.com/modern-ahocorasick/-/modern-ahocorasick-1.0.1.tgz#dec373444f51b5458ac05216a8ec376e126dd283" integrity sha512-yoe+JbhTClckZ67b2itRtistFKf8yPYelHLc7e5xAwtNAXxM6wJTUx2C7QeVSJFDzKT7bCIFyBVybPMKvmB9AA== +module-error@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/module-error/-/module-error-1.0.2.tgz#8d1a48897ca883f47a45816d4fb3e3c6ba404d86" + integrity sha512-0yuvsqSCv8LbaOKhnsQ/T5JhyFlCYLPXK3U2sgV10zoKQwzs/MyfuQUOZQ1V/6OCOJsK/TRgNVrPuPDqtdMFtA== + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -7448,6 +7514,11 @@ napi-build-utils@^1.0.1: resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== +napi-macros@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.2.2.tgz#817fef20c3e0e40a963fbf7b37d1600bd0201044" + integrity sha512-hmEVtAGYzVQpCKdbQea4skABsdXW4RUh5t5mJ2zzqowJS2OyXZTU1KhDVFhx+NlWZ4ap9mqR9TcDO3LTTttd+g== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -7506,6 +7577,11 @@ node-fetch@^3.3.0: fetch-blob "^3.1.4" formdata-polyfill "^4.0.10" +node-gyp-build@^4.3.0: + version "4.8.4" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz#8a70ee85464ae52327772a90d66c6077a900cfc8" + integrity sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ== + node-gyp@^9.0.0: version "9.4.1" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.4.1.tgz#8a1023e0d6766ecb52764cc3a734b36ff275e185" From 08bcf096411e802919acb607f7eebe56d492a90b Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Wed, 15 Jan 2025 17:00:27 +0000 Subject: [PATCH 11/85] feat: adding initial leveldb configuration --- src/main/services/hosters/datanodes.ts | 3 ++- src/types/index.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/services/hosters/datanodes.ts b/src/main/services/hosters/datanodes.ts index d77e7d51..ae144418 100644 --- a/src/main/services/hosters/datanodes.ts +++ b/src/main/services/hosters/datanodes.ts @@ -33,7 +33,8 @@ export class DatanodesApi { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36", }, - maxRedirects: 0, validateStatus: (status: number) => status === 302 || status < 400, + maxRedirects: 0, + validateStatus: (status: number) => status === 302 || status < 400, } ); diff --git a/src/types/index.ts b/src/types/index.ts index bae42702..dd631ccb 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,5 +1,6 @@ import type { Cracker, DownloadSourceStatus, Downloader } from "@shared"; import type { SteamAppDetails } from "./steam.types"; +import type { Subscription } from "./level.types"; export type GameStatus = | "active" From c59b039eb4cb5a0eae558cb77cc8cedeb57ed441 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Wed, 15 Jan 2025 17:02:40 +0000 Subject: [PATCH 12/85] fix: removing unused navigate --- src/renderer/src/pages/achievements/achievements.tsx | 2 +- .../src/pages/profile/profile-content/profile-content.tsx | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/renderer/src/pages/achievements/achievements.tsx b/src/renderer/src/pages/achievements/achievements.tsx index 605300ef..f467cf89 100644 --- a/src/renderer/src/pages/achievements/achievements.tsx +++ b/src/renderer/src/pages/achievements/achievements.tsx @@ -44,7 +44,7 @@ export default function Achievements() { .getComparedUnlockedAchievements(objectId, shop as GameShop, userId) .then(setComparedAchievements); } - }, [objectId, shop, userId]); + }, [objectId, shop, userDetails?.id, userId]); const otherUserId = userDetails?.id === userId ? null : userId; diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx index 951eb41b..71788a32 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx +++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx @@ -7,7 +7,6 @@ import { SPACING_UNIT } from "@renderer/theme.css"; import * as styles from "./profile-content.css"; import { TelescopeIcon } from "@primer/octicons-react"; import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; import { LockedProfile } from "./locked-profile"; import { ReportProfile } from "../report-profile/report-profile"; import { FriendsBox } from "./friends-box"; @@ -66,8 +65,6 @@ export function ProfileContent() { const { numberFormatter } = useFormat(); - const navigate = useNavigate(); - const usersAreFriends = useMemo(() => { return userProfile?.relation?.status === "ACCEPTED"; }, [userProfile]); @@ -148,7 +145,6 @@ export function ProfileContent() { userStats, numberFormatter, t, - navigate, statsIndex, ]); From 8b47082047b6e89adb59a39cafadcc45ea525078 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Wed, 15 Jan 2025 17:08:22 +0000 Subject: [PATCH 13/85] fix: removing unused navigate --- src/main/level/index.ts | 3 +++ src/main/level/level.ts | 4 ++++ src/main/level/sublevels/games.ts | 7 +++++++ src/main/level/sublevels/index.ts | 1 + src/main/level/sublevels/keys.ts | 8 ++++++++ src/main/services/crypto.ts | 28 ++++++++++++++++++++++++++++ src/types/level.types.ts | 23 +++++++++++++++++++++++ 7 files changed, 74 insertions(+) create mode 100644 src/main/level/index.ts create mode 100644 src/main/level/level.ts create mode 100644 src/main/level/sublevels/games.ts create mode 100644 src/main/level/sublevels/index.ts create mode 100644 src/main/level/sublevels/keys.ts create mode 100644 src/main/services/crypto.ts create mode 100644 src/types/level.types.ts diff --git a/src/main/level/index.ts b/src/main/level/index.ts new file mode 100644 index 00000000..90a34be3 --- /dev/null +++ b/src/main/level/index.ts @@ -0,0 +1,3 @@ +export { db } from "./level"; + +export * from "./sublevels"; diff --git a/src/main/level/level.ts b/src/main/level/level.ts new file mode 100644 index 00000000..382c61a5 --- /dev/null +++ b/src/main/level/level.ts @@ -0,0 +1,4 @@ +import { levelDatabasePath } from "@main/constants"; +import { Level } from "level"; + +export const db = new Level(levelDatabasePath, { valueEncoding: "json" }); diff --git a/src/main/level/sublevels/games.ts b/src/main/level/sublevels/games.ts new file mode 100644 index 00000000..bc0cad30 --- /dev/null +++ b/src/main/level/sublevels/games.ts @@ -0,0 +1,7 @@ +import { Game } from "@types"; +import { db } from "../level"; +import { levelKeys } from "./keys"; + +export const gamesSublevel = db.sublevel(levelKeys.games, { + valueEncoding: "json", +}); diff --git a/src/main/level/sublevels/index.ts b/src/main/level/sublevels/index.ts new file mode 100644 index 00000000..9d316e1a --- /dev/null +++ b/src/main/level/sublevels/index.ts @@ -0,0 +1 @@ +export * from "./games"; diff --git a/src/main/level/sublevels/keys.ts b/src/main/level/sublevels/keys.ts new file mode 100644 index 00000000..6bb54c4a --- /dev/null +++ b/src/main/level/sublevels/keys.ts @@ -0,0 +1,8 @@ +import type { GameShop } from "@types"; + +export const levelKeys = { + games: "games", + game: (shop: GameShop, objectId: string) => `${shop}:${objectId}`, + user: "user", + auth: "auth", +}; diff --git a/src/main/services/crypto.ts b/src/main/services/crypto.ts new file mode 100644 index 00000000..63a50668 --- /dev/null +++ b/src/main/services/crypto.ts @@ -0,0 +1,28 @@ +import { safeStorage } from "electron"; +import { logger } from "./logger"; + +export class Crypto { + public static encrypt(str: string) { + if (safeStorage.isEncryptionAvailable()) { + return safeStorage.encryptString(str).toString("base64"); + } else { + logger.warn( + "Encrypt method returned raw string because encryption is not available" + ); + + return str; + } + } + + public static decrypt(b64: string) { + if (safeStorage.isEncryptionAvailable()) { + return safeStorage.decryptString(Buffer.from(b64, "base64")); + } else { + logger.warn( + "Decrypt method returned raw string because encryption is not available" + ); + + return b64; + } + } +} diff --git a/src/types/level.types.ts b/src/types/level.types.ts new file mode 100644 index 00000000..490ab060 --- /dev/null +++ b/src/types/level.types.ts @@ -0,0 +1,23 @@ +export type SubscriptionStatus = "active" | "pending" | "cancelled"; + +export interface Subscription { + id: string; + status: SubscriptionStatus; + plan: { id: string; name: string }; + expiresAt: string | null; + paymentMethod: "pix" | "paypal"; +} + +export interface Auth { + accessToken: string; + refreshToken: string; + tokenExpirationTimestamp: number; +} + +export interface User { + id: string; + displayName: string; + profileImageUrl: string | null; + backgroundImageUrl: string | null; + subscription: Subscription | null; +} From 2c881a61002390a48438f6118b2ce4197c852072 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Wed, 15 Jan 2025 17:15:57 +0000 Subject: [PATCH 14/85] fix: fixing duplicate export --- src/main/entity/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/entity/index.ts b/src/main/entity/index.ts index ab0ebff9..06b543d4 100644 --- a/src/main/entity/index.ts +++ b/src/main/entity/index.ts @@ -1,6 +1,5 @@ export * from "./game.entity"; export * from "./user-preferences.entity"; export * from "./game-shop-cache.entity"; -export * from "./game.entity"; export * from "./game-achievements.entity"; export * from "./download-queue.entity"; From a23106b0b1d62b257b7aef1a9dd365d7ef92a967 Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Thu, 16 Jan 2025 02:30:09 +0000 Subject: [PATCH 15/85] feat: migrating achievements to level --- src/main/data-source.ts | 16 +- src/main/entity/game-achievements.entity.ts | 19 -- src/main/entity/game-shop-cache.entity.ts | 35 ---- src/main/entity/index.ts | 2 - src/main/events/auth/get-session-hash.ts | 2 +- src/main/events/auth/sign-out.ts | 2 +- .../events/catalogue/get-game-shop-details.ts | 34 ++-- .../events/library/reset-game-achievements.ts | 24 ++- src/main/events/misc/open-checkout.ts | 2 +- .../events/user/get-unlocked-achievements.ts | 20 +-- src/main/events/user/get-user-friends.ts | 2 +- src/main/level/sublevels/game-achievements.ts | 11 ++ src/main/level/sublevels/game-shop-cache.ts | 11 ++ src/main/level/sublevels/games.ts | 3 +- src/main/level/sublevels/index.ts | 4 + src/main/level/sublevels/keys.ts | 4 + src/main/repository.ts | 13 +- .../achievements/get-game-achievement-data.ts | 48 ++--- .../achievements/merge-achievements.ts | 66 +++---- src/main/services/hydra-api.ts | 2 +- src/main/services/user/get-user-data.ts | 2 +- .../src/components/sidebar/sidebar.tsx | 11 +- src/renderer/src/declaration.d.ts | 6 +- src/renderer/src/features/library-slice.ts | 4 +- .../pages/achievements/achievement-list.tsx | 9 +- .../src/pages/downloads/download-group.tsx | 10 +- .../src/pages/downloads/downloads.tsx | 6 +- .../pages/game-details/sidebar/sidebar.tsx | 4 +- src/types/download.types.ts | 167 ++++++++++++++++++ src/types/game.types.ts | 59 +++++++ src/types/index.ts | 115 +----------- src/types/level.types.ts | 7 + src/types/real-debrid.types.ts | 66 ------- src/types/torbox.types.ts | 77 -------- 34 files changed, 388 insertions(+), 475 deletions(-) delete mode 100644 src/main/entity/game-achievements.entity.ts delete mode 100644 src/main/entity/game-shop-cache.entity.ts create mode 100644 src/main/level/sublevels/game-achievements.ts create mode 100644 src/main/level/sublevels/game-shop-cache.ts create mode 100644 src/types/download.types.ts create mode 100644 src/types/game.types.ts delete mode 100644 src/types/real-debrid.types.ts delete mode 100644 src/types/torbox.types.ts diff --git a/src/main/data-source.ts b/src/main/data-source.ts index 05fdb04d..7414a758 100644 --- a/src/main/data-source.ts +++ b/src/main/data-source.ts @@ -1,23 +1,11 @@ import { DataSource } from "typeorm"; -import { - DownloadQueue, - Game, - GameShopCache, - UserPreferences, - GameAchievement, -} from "@main/entity"; +import { DownloadQueue, Game, UserPreferences } from "@main/entity"; import { databasePath } from "./constants"; export const dataSource = new DataSource({ type: "better-sqlite3", - entities: [ - Game, - UserPreferences, - GameShopCache, - DownloadQueue, - GameAchievement, - ], + entities: [Game, UserPreferences, DownloadQueue], synchronize: false, database: databasePath, }); diff --git a/src/main/entity/game-achievements.entity.ts b/src/main/entity/game-achievements.entity.ts deleted file mode 100644 index 0cb15f6e..00000000 --- a/src/main/entity/game-achievements.entity.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; - -@Entity("game_achievement") -export class GameAchievement { - @PrimaryGeneratedColumn() - id: number; - - @Column("text") - objectId: string; - - @Column("text") - shop: string; - - @Column("text", { nullable: true }) - unlockedAchievements: string | null; - - @Column("text", { nullable: true }) - achievements: string | null; -} diff --git a/src/main/entity/game-shop-cache.entity.ts b/src/main/entity/game-shop-cache.entity.ts deleted file mode 100644 index 3382da1c..00000000 --- a/src/main/entity/game-shop-cache.entity.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { - Entity, - PrimaryColumn, - Column, - CreateDateColumn, - UpdateDateColumn, -} from "typeorm"; -import type { GameShop } from "@types"; - -@Entity("game_shop_cache") -export class GameShopCache { - @PrimaryColumn("text", { unique: true }) - objectID: string; - - @Column("text") - shop: GameShop; - - @Column("text", { nullable: true }) - serializedData: string; - - /** - * @deprecated Use IndexedDB's `howLongToBeatEntries` instead - */ - @Column("text", { nullable: true }) - howLongToBeatSerializedData: string; - - @Column("text", { nullable: true }) - language: string; - - @CreateDateColumn() - createdAt: Date; - - @UpdateDateColumn() - updatedAt: Date; -} diff --git a/src/main/entity/index.ts b/src/main/entity/index.ts index 06b543d4..f35f643d 100644 --- a/src/main/entity/index.ts +++ b/src/main/entity/index.ts @@ -1,5 +1,3 @@ export * from "./game.entity"; export * from "./user-preferences.entity"; -export * from "./game-shop-cache.entity"; -export * from "./game-achievements.entity"; export * from "./download-queue.entity"; diff --git a/src/main/events/auth/get-session-hash.ts b/src/main/events/auth/get-session-hash.ts index 293fb62e..5848cbd7 100644 --- a/src/main/events/auth/get-session-hash.ts +++ b/src/main/events/auth/get-session-hash.ts @@ -3,7 +3,7 @@ import jwt from "jsonwebtoken"; import { registerEvent } from "../register-event"; import { db } from "@main/level"; import type { Auth } from "@types"; -import { levelKeys } from "@main/level/sublevels/keys"; +import { levelKeys } from "@main/level"; import { Crypto } from "@main/services"; const getSessionHash = async (_event: Electron.IpcMainInvokeEvent) => { diff --git a/src/main/events/auth/sign-out.ts b/src/main/events/auth/sign-out.ts index 1fb3a054..866d1ec0 100644 --- a/src/main/events/auth/sign-out.ts +++ b/src/main/events/auth/sign-out.ts @@ -4,7 +4,7 @@ import { dataSource } from "@main/data-source"; import { DownloadQueue, Game } from "@main/entity"; import { PythonRPC } from "@main/services/python-rpc"; import { db } from "@main/level"; -import { levelKeys } from "@main/level/sublevels/keys"; +import { levelKeys } from "@main/level"; const signOut = async (_event: Electron.IpcMainInvokeEvent) => { const databaseOperations = dataSource diff --git a/src/main/events/catalogue/get-game-shop-details.ts b/src/main/events/catalogue/get-game-shop-details.ts index 08366abc..39f8425b 100644 --- a/src/main/events/catalogue/get-game-shop-details.ts +++ b/src/main/events/catalogue/get-game-shop-details.ts @@ -1,10 +1,10 @@ -import { gameShopCacheRepository } from "@main/repository"; -import { getSteamAppDetails } from "@main/services"; +import { getSteamAppDetails, logger } from "@main/services"; -import type { ShopDetails, GameShop, SteamAppDetails } from "@types"; +import type { ShopDetails, GameShop } from "@types"; import { registerEvent } from "../register-event"; import { steamGamesWorker } from "@main/workers"; +import { gamesShopCacheSublevel, levelKeys } from "@main/level"; const getLocalizedSteamAppDetails = async ( objectId: string, @@ -39,35 +39,27 @@ const getGameShopDetails = async ( language: string ): Promise => { if (shop === "steam") { - const cachedData = await gameShopCacheRepository.findOne({ - where: { objectID: objectId, language }, - }); + const cachedData = await gamesShopCacheSublevel.get( + levelKeys.gameShopCacheItem(shop, objectId, language) + ); const appDetails = getLocalizedSteamAppDetails(objectId, language).then( (result) => { if (result) { - gameShopCacheRepository.upsert( - { - objectID: objectId, - shop: "steam", - language, - serializedData: JSON.stringify(result), - }, - ["objectID"] - ); + gamesShopCacheSublevel + .put(levelKeys.gameShopCacheItem(shop, objectId, language), result) + .catch((err) => { + logger.error("Could not cache game details", err); + }); } return result; } ); - const cachedGame = cachedData?.serializedData - ? (JSON.parse(cachedData?.serializedData) as SteamAppDetails) - : null; - - if (cachedGame) { + if (cachedData) { return { - ...cachedGame, + ...cachedData, objectId, } as ShopDetails; } diff --git a/src/main/events/library/reset-game-achievements.ts b/src/main/events/library/reset-game-achievements.ts index 8d52a3a6..0ea26adf 100644 --- a/src/main/events/library/reset-game-achievements.ts +++ b/src/main/events/library/reset-game-achievements.ts @@ -1,9 +1,10 @@ -import { gameAchievementRepository, gameRepository } from "@main/repository"; +import { gameRepository } from "@main/repository"; import { registerEvent } from "../register-event"; import { findAchievementFiles } from "@main/services/achievements/find-achivement-files"; import fs from "fs"; import { achievementsLogger, HydraApi, WindowManager } from "@main/services"; import { getUnlockedAchievements } from "../user/get-unlocked-achievements"; +import { gameAchievementsSublevel, levelKeys } from "@main/level"; const resetGameAchievements = async ( _event: Electron.IpcMainInvokeEvent, @@ -23,12 +24,21 @@ const resetGameAchievements = async ( } } - await gameAchievementRepository.update( - { objectId: game.objectID }, - { - unlockedAchievements: null, - } - ); + const levelKey = levelKeys.game(game.shop, game.objectID); + + await gameAchievementsSublevel + .get(levelKey) + .then(async (gameAchievements) => { + if (gameAchievements) { + await gameAchievementsSublevel.put( + levelKeys.game(game.shop, game.objectID), + { + ...gameAchievements, + unlockedAchievements: [], + } + ); + } + }); await HydraApi.delete(`/profile/games/achievements/${game.remoteId}`).then( () => diff --git a/src/main/events/misc/open-checkout.ts b/src/main/events/misc/open-checkout.ts index 76e7fe09..95d76d5b 100644 --- a/src/main/events/misc/open-checkout.ts +++ b/src/main/events/misc/open-checkout.ts @@ -3,7 +3,7 @@ import { registerEvent } from "../register-event"; import { Crypto, HydraApi } from "@main/services"; import { db } from "@main/level"; import type { Auth } from "@types"; -import { levelKeys } from "@main/level/sublevels/keys"; +import { levelKeys } from "@main/level"; const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => { const auth = await db.get(levelKeys.auth, { diff --git a/src/main/events/user/get-unlocked-achievements.ts b/src/main/events/user/get-unlocked-achievements.ts index ffa25399..78820a94 100644 --- a/src/main/events/user/get-unlocked-achievements.ts +++ b/src/main/events/user/get-unlocked-achievements.ts @@ -1,19 +1,17 @@ -import type { GameShop, UnlockedAchievement, UserAchievement } from "@types"; +import type { GameShop, UserAchievement } from "@types"; import { registerEvent } from "../register-event"; -import { - gameAchievementRepository, - userPreferencesRepository, -} from "@main/repository"; +import { userPreferencesRepository } from "@main/repository"; import { getGameAchievementData } from "@main/services/achievements/get-game-achievement-data"; +import { gameAchievementsSublevel, levelKeys } from "@main/level"; export const getUnlockedAchievements = async ( objectId: string, shop: GameShop, useCachedData: boolean ): Promise => { - const cachedAchievements = await gameAchievementRepository.findOne({ - where: { objectId, shop }, - }); + const cachedAchievements = await gameAchievementsSublevel.get( + levelKeys.game(shop, objectId) + ); const userPreferences = await userPreferencesRepository.findOne({ where: { id: 1 }, @@ -25,12 +23,10 @@ export const getUnlockedAchievements = async ( const achievementsData = await getGameAchievementData( objectId, shop, - useCachedData ? cachedAchievements : null + useCachedData ); - const unlockedAchievements = JSON.parse( - cachedAchievements?.unlockedAchievements || "[]" - ) as UnlockedAchievement[]; + const unlockedAchievements = cachedAchievements?.unlockedAchievements ?? []; return achievementsData .map((achievementData) => { diff --git a/src/main/events/user/get-user-friends.ts b/src/main/events/user/get-user-friends.ts index 7c308506..aefc7052 100644 --- a/src/main/events/user/get-user-friends.ts +++ b/src/main/events/user/get-user-friends.ts @@ -2,7 +2,7 @@ import { db } from "@main/level"; import { registerEvent } from "../register-event"; import { HydraApi } from "@main/services"; import type { User, UserFriends } from "@types"; -import { levelKeys } from "@main/level/sublevels/keys"; +import { levelKeys } from "@main/level/sublevels"; export const getUserFriends = async ( userId: string, diff --git a/src/main/level/sublevels/game-achievements.ts b/src/main/level/sublevels/game-achievements.ts new file mode 100644 index 00000000..4b1fa0c8 --- /dev/null +++ b/src/main/level/sublevels/game-achievements.ts @@ -0,0 +1,11 @@ +import type { GameAchievement } from "@types"; + +import { db } from "../level"; +import { levelKeys } from "./keys"; + +export const gameAchievementsSublevel = db.sublevel( + levelKeys.gameAchievements, + { + valueEncoding: "json", + } +); diff --git a/src/main/level/sublevels/game-shop-cache.ts b/src/main/level/sublevels/game-shop-cache.ts new file mode 100644 index 00000000..8187e5c0 --- /dev/null +++ b/src/main/level/sublevels/game-shop-cache.ts @@ -0,0 +1,11 @@ +import type { ShopDetails } from "@types"; + +import { db } from "../level"; +import { levelKeys } from "./keys"; + +export const gamesShopCacheSublevel = db.sublevel( + levelKeys.gameShopCache, + { + valueEncoding: "json", + } +); diff --git a/src/main/level/sublevels/games.ts b/src/main/level/sublevels/games.ts index bc0cad30..ce7492f1 100644 --- a/src/main/level/sublevels/games.ts +++ b/src/main/level/sublevels/games.ts @@ -1,4 +1,5 @@ -import { Game } from "@types"; +import type { Game } from "@types"; + import { db } from "../level"; import { levelKeys } from "./keys"; diff --git a/src/main/level/sublevels/index.ts b/src/main/level/sublevels/index.ts index 9d316e1a..ce61c4e2 100644 --- a/src/main/level/sublevels/index.ts +++ b/src/main/level/sublevels/index.ts @@ -1 +1,5 @@ export * from "./games"; +export * from "./game-shop-cache"; +export * from "./game-achievements"; + +export * from "./keys"; diff --git a/src/main/level/sublevels/keys.ts b/src/main/level/sublevels/keys.ts index 6bb54c4a..f2bb6f3c 100644 --- a/src/main/level/sublevels/keys.ts +++ b/src/main/level/sublevels/keys.ts @@ -5,4 +5,8 @@ export const levelKeys = { game: (shop: GameShop, objectId: string) => `${shop}:${objectId}`, user: "user", auth: "auth", + gameShopCache: "gameShopCache", + gameShopCacheItem: (shop: GameShop, objectId: string, language: string) => + `${shop}:${objectId}:${language}`, + gameAchievements: "gameAchievements", }; diff --git a/src/main/repository.ts b/src/main/repository.ts index ef120f7e..5bbfaf9f 100644 --- a/src/main/repository.ts +++ b/src/main/repository.ts @@ -1,20 +1,9 @@ import { dataSource } from "./data-source"; -import { - DownloadQueue, - Game, - GameShopCache, - UserPreferences, - GameAchievement, -} from "@main/entity"; +import { DownloadQueue, Game, UserPreferences } from "@main/entity"; export const gameRepository = dataSource.getRepository(Game); export const userPreferencesRepository = dataSource.getRepository(UserPreferences); -export const gameShopCacheRepository = dataSource.getRepository(GameShopCache); - export const downloadQueueRepository = dataSource.getRepository(DownloadQueue); - -export const gameAchievementRepository = - dataSource.getRepository(GameAchievement); diff --git a/src/main/services/achievements/get-game-achievement-data.ts b/src/main/services/achievements/get-game-achievement-data.ts index daac7e11..2dc643c1 100644 --- a/src/main/services/achievements/get-game-achievement-data.ts +++ b/src/main/services/achievements/get-game-achievement-data.ts @@ -1,40 +1,36 @@ -import { - gameAchievementRepository, - userPreferencesRepository, -} from "@main/repository"; +import { userPreferencesRepository } from "@main/repository"; import { HydraApi } from "../hydra-api"; -import type { AchievementData, GameShop } from "@types"; +import type { GameShop, SteamAchievement } from "@types"; import { UserNotLoggedInError } from "@shared"; import { logger } from "../logger"; -import { GameAchievement } from "@main/entity"; +import { gameAchievementsSublevel, levelKeys } from "@main/level"; export const getGameAchievementData = async ( objectId: string, shop: GameShop, - cachedAchievements: GameAchievement | null + useCachedData: boolean ) => { - if (cachedAchievements && cachedAchievements.achievements) { - return JSON.parse(cachedAchievements.achievements) as AchievementData[]; - } + const cachedAchievements = await gameAchievementsSublevel.get( + levelKeys.game(shop, objectId) + ); + + if (cachedAchievements && useCachedData) + return cachedAchievements.achievements; const userPreferences = await userPreferencesRepository.findOne({ where: { id: 1 }, }); - return HydraApi.get("/games/achievements", { + return HydraApi.get("/games/achievements", { shop, objectId, language: userPreferences?.language || "en", }) - .then((achievements) => { - gameAchievementRepository.upsert( - { - objectId, - shop, - achievements: JSON.stringify(achievements), - }, - ["objectId", "shop"] - ); + .then(async (achievements) => { + await gameAchievementsSublevel.put(levelKeys.game(shop, objectId), { + unlockedAchievements: cachedAchievements?.unlockedAchievements ?? [], + achievements, + }); return achievements; }) @@ -42,15 +38,9 @@ export const getGameAchievementData = async ( if (err instanceof UserNotLoggedInError) { throw err; } + logger.error("Failed to get game achievements", err); - return gameAchievementRepository - .findOne({ - where: { objectId, shop }, - }) - .then((gameAchievements) => { - return JSON.parse( - gameAchievements?.achievements || "[]" - ) as AchievementData[]; - }); + + return []; }); }; diff --git a/src/main/services/achievements/merge-achievements.ts b/src/main/services/achievements/merge-achievements.ts index dd8c877d..ac2f69d1 100644 --- a/src/main/services/achievements/merge-achievements.ts +++ b/src/main/services/achievements/merge-achievements.ts @@ -1,8 +1,5 @@ -import { - gameAchievementRepository, - userPreferencesRepository, -} from "@main/repository"; -import type { AchievementData, GameShop, UnlockedAchievement } from "@types"; +import { userPreferencesRepository } from "@main/repository"; +import type { GameShop, UnlockedAchievement } from "@types"; import { WindowManager } from "../window-manager"; import { HydraApi } from "../hydra-api"; import { getUnlockedAchievements } from "@main/events/user/get-unlocked-achievements"; @@ -10,33 +7,36 @@ import { Game } from "@main/entity"; import { publishNewAchievementNotification } from "../notifications"; import { SubscriptionRequiredError } from "@shared"; import { achievementsLogger } from "../logger"; +import { gameAchievementsSublevel, levelKeys } from "@main/level"; const saveAchievementsOnLocal = async ( objectId: string, shop: GameShop, - achievements: UnlockedAchievement[], + unlockedAchievements: UnlockedAchievement[], sendUpdateEvent: boolean ) => { - return gameAchievementRepository - .upsert( - { - objectId, - shop, - unlockedAchievements: JSON.stringify(achievements), - }, - ["objectId", "shop"] - ) - .then(() => { - if (!sendUpdateEvent) return; + const levelKey = levelKeys.game(shop, objectId); - return getUnlockedAchievements(objectId, shop, true) - .then((achievements) => { - WindowManager.mainWindow?.webContents.send( - `on-update-achievements-${objectId}-${shop}`, - achievements - ); - }) - .catch(() => {}); + return gameAchievementsSublevel + .get(levelKey) + .then(async (gameAchievement) => { + if (gameAchievement) { + await gameAchievementsSublevel.put(levelKey, { + ...gameAchievement, + unlockedAchievements: unlockedAchievements, + }); + + if (!sendUpdateEvent) return; + + return getUnlockedAchievements(objectId, shop, true) + .then((achievements) => { + WindowManager.mainWindow?.webContents.send( + `on-update-achievements-${objectId}-${shop}`, + achievements + ); + }) + .catch(() => {}); + } }); }; @@ -46,22 +46,12 @@ export const mergeAchievements = async ( publishNotification: boolean ) => { const [localGameAchievement, userPreferences] = await Promise.all([ - gameAchievementRepository.findOne({ - where: { - objectId: game.objectID, - shop: game.shop, - }, - }), + gameAchievementsSublevel.get(levelKeys.game(game.shop, game.objectID)), userPreferencesRepository.findOne({ where: { id: 1 } }), ]); - const achievementsData = JSON.parse( - localGameAchievement?.achievements || "[]" - ) as AchievementData[]; - - const unlockedAchievements = JSON.parse( - localGameAchievement?.unlockedAchievements || "[]" - ).filter((achievement) => achievement.name) as UnlockedAchievement[]; + const achievementsData = localGameAchievement?.achievements ?? []; + const unlockedAchievements = localGameAchievement?.unlockedAchievements ?? []; const newAchievementsMap = new Map( achievements.reverse().map((achievement) => { diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index 6cf9a8af..5f7a5034 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -10,7 +10,7 @@ import { appVersion } from "@main/constants"; import { getUserData } from "./user/get-user-data"; import { isFuture, isToday } from "date-fns"; import { db } from "@main/level"; -import { levelKeys } from "@main/level/sublevels/keys"; +import { levelKeys } from "@main/level/sublevels"; import type { Auth, User } from "@types"; import { Crypto } from "./crypto"; diff --git a/src/main/services/user/get-user-data.ts b/src/main/services/user/get-user-data.ts index e6cf1c71..ed07c61e 100644 --- a/src/main/services/user/get-user-data.ts +++ b/src/main/services/user/get-user-data.ts @@ -3,7 +3,7 @@ import { HydraApi } from "../hydra-api"; import { UserNotLoggedInError } from "@shared"; import { logger } from "../logger"; import { db } from "@main/level"; -import { levelKeys } from "@main/level/sublevels/keys"; +import { levelKeys } from "@main/level/sublevels"; export const getUserData = async () => { return HydraApi.get(`/profile/me`) diff --git a/src/renderer/src/components/sidebar/sidebar.tsx b/src/renderer/src/components/sidebar/sidebar.tsx index 355d04b2..ae22f552 100644 --- a/src/renderer/src/components/sidebar/sidebar.tsx +++ b/src/renderer/src/components/sidebar/sidebar.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useLocation, useNavigate } from "react-router-dom"; -import type { LibraryGame } from "@types"; +import type { Game } from "@types"; import { TextField } from "@renderer/components"; import { @@ -35,7 +35,7 @@ export function Sidebar() { const { library, updateLibrary } = useLibrary(); const navigate = useNavigate(); - const [filteredLibrary, setFilteredLibrary] = useState([]); + const [filteredLibrary, setFilteredLibrary] = useState([]); const [isResizing, setIsResizing] = useState(false); const [sidebarWidth, setSidebarWidth] = useState( @@ -117,7 +117,7 @@ export function Sidebar() { }; }, [isResizing]); - const getGameTitle = (game: LibraryGame) => { + const getGameTitle = (game: Game) => { if (lastPacket?.game.id === game.id) { return t("downloading", { title: game.title, @@ -140,10 +140,7 @@ export function Sidebar() { } }; - const handleSidebarGameClick = ( - event: React.MouseEvent, - game: LibraryGame - ) => { + const handleSidebarGameClick = (event: React.MouseEvent, game: Game) => { const path = buildGameDetailsPath({ ...game, objectId: game.objectID, diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 88f3297f..2ee60347 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -2,7 +2,6 @@ import type { CatalogueCategory } from "@shared"; import type { AppUpdaterEvent, Game, - LibraryGame, GameShop, HowLongToBeatCategory, ShopDetails, @@ -23,7 +22,6 @@ import type { UserStats, UserDetails, FriendRequestSync, - GameAchievement, GameArtifact, LudusaviBackup, UserAchievement, @@ -77,7 +75,7 @@ declare global { onUpdateAchievements: ( objectId: string, shop: GameShop, - cb: (achievements: GameAchievement[]) => void + cb: (achievements: UserAchievement[]) => void ) => () => Electron.IpcRenderer; getPublishers: () => Promise; getDevelopers: () => Promise; @@ -102,7 +100,7 @@ declare global { winePrefixPath: string | null ) => Promise; verifyExecutablePathInUse: (executablePath: string) => Promise; - getLibrary: () => Promise; + getLibrary: () => Promise; openGameInstaller: (gameId: number) => Promise; openGameInstallerPath: (gameId: number) => Promise; openGameExecutablePath: (gameId: number) => Promise; diff --git a/src/renderer/src/features/library-slice.ts b/src/renderer/src/features/library-slice.ts index 6c95aa79..f536ace7 100644 --- a/src/renderer/src/features/library-slice.ts +++ b/src/renderer/src/features/library-slice.ts @@ -1,10 +1,10 @@ import { createSlice } from "@reduxjs/toolkit"; import type { PayloadAction } from "@reduxjs/toolkit"; -import type { LibraryGame } from "@types"; +import type { Game } from "@types"; export interface LibraryState { - value: LibraryGame[]; + value: Game[]; } const initialState: LibraryState = { diff --git a/src/renderer/src/pages/achievements/achievement-list.tsx b/src/renderer/src/pages/achievements/achievement-list.tsx index ef178b50..6066f241 100644 --- a/src/renderer/src/pages/achievements/achievement-list.tsx +++ b/src/renderer/src/pages/achievements/achievement-list.tsx @@ -47,7 +47,14 @@ export function AchievementList({ achievements }: AchievementListProps) {

{achievement.description}

-
+
{achievement.points != undefined ? (
void; openGameInstaller: (gameId: number) => void; @@ -65,7 +65,7 @@ export function DownloadGroup({ resumeSeeding, } = useDownload(); - const getFinalDownloadSize = (game: LibraryGame) => { + const getFinalDownloadSize = (game: Game) => { const isGameDownloading = lastPacket?.game.id === game.id; if (game.fileSize) return formatBytes(game.fileSize); @@ -86,7 +86,7 @@ export function DownloadGroup({ return map; }, [seedingStatus]); - const getGameInfo = (game: LibraryGame) => { + const getGameInfo = (game: Game) => { const isGameDownloading = lastPacket?.game.id === game.id; const finalDownloadSize = getFinalDownloadSize(game); const seedingStatus = seedingMap.get(game.id); @@ -165,7 +165,7 @@ export function DownloadGroup({ return

{t(game.status as string)}

; }; - const getGameActions = (game: LibraryGame): DropdownMenuItem[] => { + const getGameActions = (game: Game): DropdownMenuItem[] => { const isGameDownloading = lastPacket?.game.id === game.id; const deleting = isGameDeleting(game.id); diff --git a/src/renderer/src/pages/downloads/downloads.tsx b/src/renderer/src/pages/downloads/downloads.tsx index 5c5a121a..41dbae90 100644 --- a/src/renderer/src/pages/downloads/downloads.tsx +++ b/src/renderer/src/pages/downloads/downloads.tsx @@ -7,7 +7,7 @@ import { BinaryNotFoundModal } from "../shared-modals/binary-not-found-modal"; import * as styles from "./downloads.css"; import { DeleteGameModal } from "./delete-game-modal"; import { DownloadGroup } from "./download-group"; -import type { LibraryGame, SeedingStatus } from "@types"; +import type { Game, SeedingStatus } from "@types"; import { orderBy } from "lodash-es"; import { ArrowDownIcon } from "@primer/octicons-react"; @@ -49,8 +49,8 @@ export default function Downloads() { setShowDeleteModal(true); }; - const libraryGroup: Record = useMemo(() => { - const initialValue: Record = { + const libraryGroup: Record = useMemo(() => { + const initialValue: Record = { downloading: [], queued: [], complete: [], diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx index 7787b22a..d3a65ae5 100644 --- a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx +++ b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx @@ -23,7 +23,7 @@ import { buildGameAchievementPath } from "@renderer/helpers"; import { SPACING_UNIT } from "@renderer/theme.css"; import { useSubscription } from "@renderer/hooks/use-subscription"; -const fakeAchievements: UserAchievement[] = [ +const achievementsPlaceholder: UserAchievement[] = [ { displayName: "Timber!!", name: "", @@ -140,7 +140,7 @@ export function Sidebar() {

{t("sign_in_to_see_achievements")}

diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index eaf5cb49..a68db401 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -40,7 +40,9 @@ declare global { interface Electron { /* Torrenting */ - startGameDownload: (payload: StartGameDownloadPayload) => Promise; + startGameDownload: ( + payload: StartGameDownloadPayload + ) => Promise<{ ok: boolean; error?: string }>; cancelGameDownload: (shop: GameShop, objectId: string) => Promise; pauseGameDownload: (shop: GameShop, objectId: string) => Promise; resumeGameDownload: (shop: GameShop, objectId: string) => Promise; diff --git a/src/renderer/src/features/toast-slice.ts b/src/renderer/src/features/toast-slice.ts index 44abf53a..f5df1d1c 100644 --- a/src/renderer/src/features/toast-slice.ts +++ b/src/renderer/src/features/toast-slice.ts @@ -6,6 +6,7 @@ export interface ToastState { title: string; message?: string; type: ToastProps["type"]; + duration?: number; visible: boolean; } @@ -13,6 +14,7 @@ const initialState: ToastState = { title: "", message: "", type: "success", + duration: 5000, visible: false, }; @@ -24,6 +26,7 @@ export const toastSlice = createSlice({ state.title = action.payload.title; state.message = action.payload.message; state.type = action.payload.type; + state.duration = action.payload.duration ?? 5000; state.visible = true; }, closeToast: (state) => { diff --git a/src/renderer/src/hooks/use-download.ts b/src/renderer/src/hooks/use-download.ts index 2a21dea2..b84ac515 100644 --- a/src/renderer/src/hooks/use-download.ts +++ b/src/renderer/src/hooks/use-download.ts @@ -29,10 +29,11 @@ export function useDownload() { const startDownload = async (payload: StartGameDownloadPayload) => { dispatch(clearDownload()); - const game = await window.electron.startGameDownload(payload); + const response = await window.electron.startGameDownload(payload); - await updateLibrary(); - return game; + if (response.ok) updateLibrary(); + + return response; }; const pauseDownload = async (shop: GameShop, objectId: string) => { diff --git a/src/renderer/src/hooks/use-toast.ts b/src/renderer/src/hooks/use-toast.ts index 5e08a7ab..8b4c3e0f 100644 --- a/src/renderer/src/hooks/use-toast.ts +++ b/src/renderer/src/hooks/use-toast.ts @@ -6,12 +6,13 @@ export function useToast() { const dispatch = useAppDispatch(); const showSuccessToast = useCallback( - (title: string, message?: string) => { + (title: string, message?: string, duration?: number) => { dispatch( showToast({ title, message, type: "success", + duration, }) ); }, @@ -19,12 +20,13 @@ export function useToast() { ); const showErrorToast = useCallback( - (title: string, message?: string) => { + (title: string, message?: string, duration?: number) => { dispatch( showToast({ title, message, type: "error", + duration, }) ); }, @@ -32,12 +34,13 @@ export function useToast() { ); const showWarningToast = useCallback( - (title: string, message?: string) => { + (title: string, message?: string, duration?: number) => { dispatch( showToast({ title, message, type: "warning", + duration, }) ); }, diff --git a/src/renderer/src/pages/downloads/downloads.tsx b/src/renderer/src/pages/downloads/downloads.tsx index c9a2a9e5..b4f8c20b 100644 --- a/src/renderer/src/pages/downloads/downloads.tsx +++ b/src/renderer/src/pages/downloads/downloads.tsx @@ -8,7 +8,7 @@ import * as styles from "./downloads.css"; import { DeleteGameModal } from "./delete-game-modal"; import { DownloadGroup } from "./download-group"; import type { GameShop, LibraryGame, SeedingStatus } from "@types"; -import { orderBy } from "lodash-es"; +import { orderBy, sortBy } from "lodash-es"; import { ArrowDownIcon } from "@primer/octicons-react"; export default function Downloads() { @@ -58,21 +58,24 @@ export default function Downloads() { complete: [], }; - const result = library.reduce((prev, next) => { - /* Game has been manually added to the library or has been canceled */ - if (!next.download?.status || next.download?.status === "removed") - return prev; + const result = sortBy(library, (game) => game.download?.timestamp).reduce( + (prev, next) => { + /* Game has been manually added to the library or has been canceled */ + if (!next.download?.status || next.download?.status === "removed") + return prev; - /* Is downloading */ - if (lastPacket?.gameId === next.id) - return { ...prev, downloading: [...prev.downloading, next] }; + /* Is downloading */ + if (lastPacket?.gameId === next.id) + return { ...prev, downloading: [...prev.downloading, next] }; - /* Is either queued or paused */ - if (next.download.queued || next.download?.status === "paused") - return { ...prev, queued: [...prev.queued, next] }; + /* Is either queued or paused */ + if (next.download.queued || next.download?.status === "paused") + return { ...prev, queued: [...prev.queued, next] }; - return { ...prev, complete: [...prev.complete, next] }; - }, initialValue); + return { ...prev, complete: [...prev.complete, next] }; + }, + initialValue + ); const queued = orderBy(result.queued, (game) => game.download?.timestamp, [ "desc", diff --git a/src/renderer/src/pages/game-details/game-details.tsx b/src/renderer/src/pages/game-details/game-details.tsx index 4fbcc855..e778ffef 100644 --- a/src/renderer/src/pages/game-details/game-details.tsx +++ b/src/renderer/src/pages/game-details/game-details.tsx @@ -102,19 +102,23 @@ export default function GameDetails() { downloader: Downloader, downloadPath: string ) => { - await startDownload({ + const response = await startDownload({ repackId: repack.id, objectId: objectId!, title: gameTitle, downloader, - shop: shop as GameShop, + shop, downloadPath, uri: selectRepackUri(repack, downloader), }); - await updateGame(); - setShowRepacksModal(false); - setShowGameOptionsModal(false); + if (response.ok) { + await updateGame(); + setShowRepacksModal(false); + setShowGameOptionsModal(false); + } + + return response; }; const handleNSFWContentRefuse = () => { @@ -123,10 +127,7 @@ export default function GameDetails() { }; return ( - + {({ showCloudSyncModal, diff --git a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx index 9da8ea2e..786c6793 100644 --- a/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/download-settings-modal.tsx @@ -18,7 +18,7 @@ export interface DownloadSettingsModalProps { repack: GameRepack, downloader: Downloader, downloadPath: string - ) => Promise; + ) => Promise<{ ok: boolean; error?: string }>; repack: GameRepack | null; } @@ -27,7 +27,7 @@ export function DownloadSettingsModal({ onClose, startDownload, repack, -}: DownloadSettingsModalProps) { +}: Readonly) { const { t } = useTranslation("game_details"); const { showErrorToast } = useToast(); @@ -117,20 +117,30 @@ export function DownloadSettingsModal({ } }; - const handleStartClick = () => { + const handleStartClick = async () => { if (repack) { setDownloadStarting(true); - startDownload(repack, selectedDownloader!, selectedPath) - .then(() => { + try { + const response = await startDownload( + repack, + selectedDownloader!, + selectedPath + ); + + if (response.ok) { onClose(); - }) - .catch((error) => { - showErrorToast(t("download_error"), error.message); - }) - .finally(() => { - setDownloadStarting(false); - }); + return; + } else if (response.error) { + showErrorToast(t("download_error"), t(response.error), 4_000); + } + } catch (error) { + if (error instanceof Error) { + showErrorToast(t("download_error"), error.message, 4_000); + } + } finally { + setDownloadStarting(false); + } } }; diff --git a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx index b2b7b6f8..7d176de7 100644 --- a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx @@ -19,7 +19,7 @@ export interface RepacksModalProps { repack: GameRepack, downloader: Downloader, downloadPath: string - ) => Promise; + ) => Promise<{ ok: boolean; error?: string }>; onClose: () => void; } @@ -27,7 +27,7 @@ export function RepacksModal({ visible, startDownload, onClose, -}: RepacksModalProps) { +}: Readonly) { const [filteredRepacks, setFilteredRepacks] = useState([]); const [repack, setRepack] = useState(null); const [showSelectFolderModal, setShowSelectFolderModal] = useState(false); @@ -111,7 +111,7 @@ export function RepacksModal({

{repack.fileSize} - {repack.repacker} -{" "} - {repack.uploadDate ? formatDate(repack.uploadDate!) : ""} + {repack.uploadDate ? formatDate(repack.uploadDate) : ""}

); diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 3f3e0a8b..f26128f5 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -49,3 +49,10 @@ export enum AuthPage { UpdateEmail = "/update-email", UpdatePassword = "/update-password", } + +export enum DownloadError { + NotCachedInRealDebrid = "download_error_not_cached_in_real_debrid", + NotCachedInTorbox = "download_error_not_cached_in_torbox", + GofileQuotaExceeded = "download_error_gofile_quota_exceeded", + RealDebridAccountNotAuthorized = "download_error_real_debrid_account_not_authorized", +} From 220b3620d4aa09b7a4e1e4a5bfce45fe6a6aaf46 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sat, 1 Feb 2025 21:09:06 -0300 Subject: [PATCH 79/85] feat: add torbox error handling --- src/main/events/torrenting/start-game-download.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/events/torrenting/start-game-download.ts b/src/main/events/torrenting/start-game-download.ts index 96e3499a..8b5f1918 100644 --- a/src/main/events/torrenting/start-game-download.ts +++ b/src/main/events/torrenting/start-game-download.ts @@ -76,10 +76,10 @@ const startGameDownload = async ( queued: true, }; - await downloadsSublevel.put(gameKey, download); - try { - await DownloadManager.startDownload(download); + await DownloadManager.startDownload(download).then(() => { + return downloadsSublevel.put(gameKey, download); + }); const updatedGame = await gamesSublevel.get(gameKey); @@ -113,6 +113,10 @@ const startGameDownload = async ( error: DownloadError.RealDebridAccountNotAuthorized, }; } + + if (downloader === Downloader.TorBox) { + return { ok: false, error: err.response?.data?.detail }; + } } if (err instanceof Error) { From ba6d8dd6a48001823b581ee399424b19f515af6e Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sat, 1 Feb 2025 21:12:15 -0300 Subject: [PATCH 80/85] feat: remove unnecessary inline css --- src/renderer/src/pages/settings/settings-real-debrid.tsx | 8 +------- src/renderer/src/pages/settings/settings-torbox.tsx | 8 +------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/src/renderer/src/pages/settings/settings-real-debrid.tsx b/src/renderer/src/pages/settings/settings-real-debrid.tsx index 300acc8e..0d4e5108 100644 --- a/src/renderer/src/pages/settings/settings-real-debrid.tsx +++ b/src/renderer/src/pages/settings/settings-real-debrid.tsx @@ -108,13 +108,7 @@ export function SettingsRealDebrid() { }, }} rightContent={ - } diff --git a/src/renderer/src/pages/settings/settings-torbox.tsx b/src/renderer/src/pages/settings/settings-torbox.tsx index fa7a41f5..ce4b5fcc 100644 --- a/src/renderer/src/pages/settings/settings-torbox.tsx +++ b/src/renderer/src/pages/settings/settings-torbox.tsx @@ -100,13 +100,7 @@ export function SettingsTorbox() { }, }} rightContent={ - } From 4e2427dbefdd97b922f2cd00765ddac74b2173fc Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sat, 1 Feb 2025 21:23:22 -0300 Subject: [PATCH 81/85] feat: refactor settings tabs --- src/renderer/src/pages/settings/settings.tsx | 37 +++++++++++--------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/src/renderer/src/pages/settings/settings.tsx b/src/renderer/src/pages/settings/settings.tsx index c752890a..a3a955e2 100644 --- a/src/renderer/src/pages/settings/settings.tsx +++ b/src/renderer/src/pages/settings/settings.tsx @@ -23,14 +23,26 @@ export default function Settings() { const categories = useMemo(() => { const categories = [ - t("general"), - t("behavior"), - t("download_sources"), - "Torbox", - "Real-Debrid", + { tabLabel: t("general"), contentTitle: t("general") }, + { tabLabel: t("behavior"), contentTitle: t("behavior") }, + { tabLabel: t("download_sources"), contentTitle: t("download_sources") }, + { + tabLabel: ( + <> + TorBox + Torbox + + ), + contentTitle: "TorBox", + }, + { tabLabel: "Real-Debrid", contentTitle: "Real-Debrid" }, ]; - if (userDetails) return [...categories, t("account")]; + if (userDetails) + return [ + ...categories, + { tabLabel: t("account"), contentTitle: t("account") }, + ]; return categories; }, [userDetails, t]); @@ -68,25 +80,18 @@ export default function Settings() {
{categories.map((category, index) => ( ))}
-

{categories[currentCategoryIndex]}

+

{categories[currentCategoryIndex].contentTitle}

{renderCategory()}
From b0df4d8fd7a949163b644fa5dea9a1079e3486df Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sun, 2 Feb 2025 01:16:32 -0300 Subject: [PATCH 82/85] feat: adjustments --- src/renderer/src/components/toast/toast.scss | 4 ++-- .../game-details/cloud-sync-modal/cloud-sync-modal.tsx | 8 -------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/renderer/src/components/toast/toast.scss b/src/renderer/src/components/toast/toast.scss index b112641b..b6cca04b 100644 --- a/src/renderer/src/components/toast/toast.scss +++ b/src/renderer/src/components/toast/toast.scss @@ -7,8 +7,8 @@ background-color: globals.$dark-background-color; border-radius: 4px; border: solid 1px globals.$border-color; - right: 0; - bottom: 0; + right: 16px; + bottom: 26px + globals.$spacing-unit; overflow: hidden; display: flex; flex-direction: column; diff --git a/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx b/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx index 5c5d5afb..4544ab3b 100644 --- a/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx +++ b/src/renderer/src/pages/game-details/cloud-sync-modal/cloud-sync-modal.tsx @@ -189,14 +189,6 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) { - {uploadingBackup && ( - - )} -

{t("backups")}

From b2374857dbe3b1e6feb4647e6cb3733874f35bfd Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sun, 2 Feb 2025 01:18:00 -0300 Subject: [PATCH 83/85] feat: add missing flex-direction --- src/renderer/src/components/toast/toast.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/src/renderer/src/components/toast/toast.scss b/src/renderer/src/components/toast/toast.scss index b6cca04b..e5320231 100644 --- a/src/renderer/src/components/toast/toast.scss +++ b/src/renderer/src/components/toast/toast.scss @@ -34,6 +34,7 @@ &__message-container { display: flex; gap: globals.$spacing-unit; + flex-direction: column; } &__message { From 56d2b4706ea40608ba7c4fb023aa1af94e3d0147 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sun, 2 Feb 2025 12:46:56 -0300 Subject: [PATCH 84/85] feat: remove inline css --- src/renderer/src/pages/downloads/download-group.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/src/pages/downloads/download-group.tsx b/src/renderer/src/pages/downloads/download-group.tsx index 1d95dace..d5e568fb 100644 --- a/src/renderer/src/pages/downloads/download-group.tsx +++ b/src/renderer/src/pages/downloads/download-group.tsx @@ -291,7 +291,7 @@ export function DownloadGroup({ alt="TorBox" style={{ width: 13 }} /> - TorBox + TorBox ) : ( From 6a3930c36edff59517aaa54da3fa51dcb99058a3 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Sun, 2 Feb 2025 12:55:00 -0300 Subject: [PATCH 85/85] fix: css for checkbox --- .../src/components/checkbox-field/checkbox-field.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/renderer/src/components/checkbox-field/checkbox-field.scss b/src/renderer/src/components/checkbox-field/checkbox-field.scss index fb8b4131..85ab4149 100644 --- a/src/renderer/src/components/checkbox-field/checkbox-field.scss +++ b/src/renderer/src/components/checkbox-field/checkbox-field.scss @@ -15,6 +15,8 @@ &__checkbox { width: 20px; height: 20px; + min-width: 20px; + min-height: 20px; border-radius: 4px; background-color: globals.$dark-background-color; display: flex; @@ -45,6 +47,9 @@ &__label { cursor: pointer; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; &:has(+ input:disabled) { cursor: not-allowed;