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 zcmV-+FNx4nNk&F)F8}~nMM6+kP&iCtF8}~9|G|F{RSP4xZ6i7Q|F4s__N>>4m;n7^ z>{A69WCJQm*X9xE_yF&Qun%mPlXUL1%sU@Z-bukhA!X5L#x(!`dE0WHTc6MBegyr% z`Wf8y{kQEt>$94(?&o#5_y7O znA^sXCw1}wob0OXX>6OV*tTt+S!^e(a;K>>PSuX>tYB!{PU{RQNRp&TlHR)?*h$+x zW@ctai#o=JD(ja)jwDHuWVXLp{q>kwhGS;FI0jWHK>vTf?e3Rf?4-7B+r4YrnQ8yV zjOsdMS9eBj+qR8uS#8_4`<(zf{r|tZ6Gw=+z4xBpd+)vX-g~!7z4tli{Eq|#px|Qx zvR?9eV$8&xJw9Tl8o)plJUa@y6-HPw@a#+|hyhjK251J( zbaY({2;fM-MPx(=dux+7D>c%LD*bc{3b(>2GAxH&ej$W9psz|0Y| z2hft4nHeJwpgDK*49sp$%`VS?nVFfHDtht^y5t!&bA(ZMR7cvOIqKNVj-MR0JOW&X z$l>1%m2JuF#w#5&GlX1v(M!!(>dcY#w&W2ksZ0}il9?goX3DKy128Q!hs^eyfFbM5 z5<|qyu#5o&0RRX!y?R}??y}pmZ8tC5#@1SxnQIaR07xR~|F4;z?d~oR!ESME1r_kF z{}W(-FA9q?McKU*WhM$+xEB=_brIbX1@TUnsYj?2#fh>+`(q~`N%W1Vh$s}`M6seq zqKTr%v9qFQqQj!5jiD_Wp+rq)Cb|~8_`zo;YElYnqP(K_L|0;0Z~lDp${Wr^c|;$J z{t&&0-LUpUp}b0pR*UY(I=Q!^wW3T>bdVC25%m-u zjdfP^K-6b@!jmXP#`mJ*u}+Ilh`zs!uq294lp&hk6zh1?Ddm;%R^g~c)Jim0bTih& z{$H1lK6p&U2NzA~5oSc`^AT+kJ&kE8 z+M3sU&l+At5u&Q1-$f5&incEI=T-xYsIsW9=yXikYu(l33MnUd0iyFUg)c$I02z5g zi6~L@mS|ZmrgH6SpK=d_h^T~Uh3IZf?f0CL=yg#{@DP<0brl_qsopB;ZDB)3ij1DC z9#enS2V6$D_*79#(bT4xfldFq=uZt3EuvJ zFZ%faqXQn`KAurY>GHqJyN{x?(GjO5<7XLpHLg+gYW|l=k&GA78Ec|NQ)|?=g^JpU z7K?62r#!U%oaaJSt%s;rLv+rDhwh{%14TzJ zrmA1bCMr=w(Uc#J4*R1!E^4^?%9V_~NB5r5ao_XkeonEf&no)p_XkGj9oTyt6Q%#7 zRy~|(`xQnfzCsz>A5NuOK36p4Wuh~`%;|k@i#mzYMF0HJ=+r}<-fpVWx5|jmD^s*) zZ=-9A)<5uDmGtN~WULe2iw@rWq))krI#d#s5`8B+79G7T8uZ$OD5OB5d_6^{%F)@& zZ`AXvv}g528KU{M==8NCy2aJkoMl_IXx^Pf=f9JM%V?R5IJG&vhi-^o*znMYr!zae zK+(SEkDmDazbfi+ zxE8&$w!%kumP%53#W{(#wMH*(J-9i^Nd8t0Ikk6djo#WS;}03l)T2dILDYX*(QBvm z!kgTp8dtfiy~@#Z%e`duO;lVhN{tuuq|u9?^kN!II+0gR(aPrN$<0%mnkY#hsyzlA zRrKicAM9&Y84~4_(e-1SqE|P4?B9NsCbWo}FFbnq!Z)Br3zqHSezt$~^8P<7qv_9S zK&emuw~1G4EqZ#b^`ZNeyl;m^pNMufM2~Oy-$&c>lHc9va+Ra!4;1Ydz3FkEdQfY; z`B4Eke~l07Q$6i6@@n6|7FDozVxr-qN**@WUxZ{NirR@b?rqdV(VuI-hb7;V(c-mQ zqbjzpwWx(x9h0c^pNP&xb=>`ZypUt^CQ&ib57TOj3fVL*89yW{=0~48&p(W6`GcK{(AnXu+Kj`8E<;RryiGR@K8}{hkD%O`c(HTy_Kl_gO{V)mN)+bHre$= z2~YW?%~5x6@=veV>Goj7t}v?a6^hm^+S!ijDto?BRN*)3d6mcHWU?tL>LEJ${85RY zf9bZkhkJeM_2hq@R7X|bfp7X$x7x!+FMMyKGVkre7oBVp-Ki?dB^uaTi|Sl!-HxBE zI?~DA!>=|(m2UXedsq(_Iwn!=uZwzJJJ(k$xlXc)l~G$XPIM!xcK^5A%SM+e&1k@# z)S`0N?qtA>O3t#wi#@p3sNk&!x7Zzal>D!go43;xmAvV8?%F|C|BQB3HkYEJi!SZz z8Lhrsls;AT`O}u8s+W6-eiD^)iR+!(l-@Jy`%Q{oxn5_uo+&j%m2VI&pW0=(K+C9q zyL3_QuTV5uRInxUl2N$tsy}~J{O4cwzN2Paqp0y8M)m*0t)~x<*d|eESwO4wzYL+CM zn1d3Hc<@?ah_U~%9n?az;wAHnlhNZxYcW99Hve%&HL_ucPw4*V7DMHRJn+mj;7>Qb z90O)~)BiN(j_Ji$Ty!x8&c9so7eA&H+n%1+T8_c9e62l`Q6{af_al49K-&8wuQ!#x zNwn|^V=%qK51r?X(x!ShqAw0G2G!=vS&wR}B%{y*H{|(aV151#S)iy;dNf|alg1$X zq*u_G5@Y)HKr3-bTH#ulsjaX$B#rg!rB*83^Z!f#dE8RVIRKI)i51So0m#o2Fpf`l zz=WQ^uAI!hZ&Vr-KdOa~dHKN&uN&(y>QLe-e2oyt_SFhZ;N=TYc=!y++jkNs4T>J! zqDQ~z(Fz}^42U$4OUUq1sKA9(?7l}@G$iPi3ZQ(DhXG${OxFyM>wuzZMpATXjqx?RH0(i@!hzM(ydEIF z9!dNJrBe;8r*B*=)0~4pf!yD^n7+!hVbpgI5;}_~u(Kn|+Iosg<@#G2%zFMy2b9lv z606yeNj9{dEOPpurrkjP-6;IG18QbIB)OJ&nWr^-lR~a;ZMmN8^8Z0)FM#z7Xh$n0 z?H~x`;?~-}s*|CNSLQ4UfK?pSsIst;d}-v~k2;9+w88<^y+UGj0ZjF-ov4#rTfe=& z@OBjL+W`bU1D0=F4-36)AIRj+R=B_XVmS390s#O?4}tX#9(!vjA(k6kYoB8%`0PRb zIk3b5RuzbGWoipL+_;y1KZx_N!U0r0Mv`lozzlEO4Jx^=)zh*bAh{k%_y&Z%2FdY0 z^J0nT9Y`X%tQGF3I2%rV-I22-16Fo%!`jBtSnm4dL!l=R`p*HRy$DtvkO^&ME8ASP zmvK9gf4_+XhX<$28B`F7iC)cyq_Enz@1z$W!;J^|Z zSmi;C8VeiMmqu>aTfZO7yftJ^jYna?QxfU95|FnPIe7>1?n8J2SE`-A*G{>8(E+%zhk=`Ex*D zodoRFDXnh@nIadldYZNa`M0C+U!c2=LUO!yJuT#l6LO6a-ea5}?SY9reFrKj>?&Z- z4QdCgB;|%;$TP2lU254A6Gpyw?xxok@ zI-YkAzz{H4>Lqz^@(K7aK5v zm#)ATwN<TX-~J%(UB^eCKwBlv9SC@4R&IJ(L2Z5D0P)oiLdv_5_4Mr* zOD*Ru1+jH8{p9DvDR1ZL7y)3{hnKbQ;wWgXx9F`q9?X2+mjk-*N??yqDg)cuq(JRv zUv_t_`rD2N3h(ylz+%vyNNC<~^StUC1!i~KqML2e-+!D{0VNkSFs znwC{ChI_kr35oZqvxE1bDnl6<4q#_SwWZr>6nszH)3F+Lc{k=EY~D!U|3BYB6&M50 z2{0hQu1zgH#}sf+`;&vh=S2;3?hp4JXWvn|%}bFfcRg@mNRZ@Df4&c83b3CH_wya4 z=YM9u)!xG_DfRu&b6>_3uH^xsF#*;)XdJATgsg%p+}pK@jlWKsX01E9hEhs>uz&UA zK8}hb0AN&rRXo(#F|~zU!Su8}9IFxMx7EX7;d)v_+q9>*n@E%f#s$!@fSKB|8#D@> zr|o824h){=)k)Jg^BrcFw#~+;OE5Mp|I$1AL83tEZd-)=diSZBubD3uk+gl^URHz> zVQheX_UKxPi4`2-Uan2l-P44}v}LQ-P=CE!nP>31(E)a2LLPbVViXKdyLMRQx}xjN zUyZ)wKk9$WN~B9H4;&gFkZX#MbGu2U0Qkx90RKr&88&`)_bjRZa_;Mx#4{65^ry-P}eOnWZ9JIQFEZ1%8=qhMi(K;dJQAIylhw%(#nMSkARzUb*( zd#)bGj^mb%SVIH1r&OvaEQ$c2K>}8DAd|gs2bdK3_0x8@Er*6KOWW4GUoNSV_Sm~a z1RQ{2LXvxBzE$i^3i_r2Acl$9Tbd%3o;@s~+cdarA!s%%oed=Wzi?2&POYAZO> z$nT!EhkZ37c3m~_7ObZ=RAsw5mN{U|0FCo!mbH8!Uwhi{z`$94Rd4+6rKGACyW(9N zHDolyJ&%7% zTXu2{MUc(=7}Eq6H*&yE{&Pu@|2*wm42xWqcbxglK~f}H6`S$^A0ltM-CI-nTBHzwGkL!E*w$l-?>Ap_YxzI(1Bg2<*9mal;7g^5`+Mi9M;jJeF@);JQ{g000$X$`JCw z=K3*9pkX5ksE?m_cY#J;9pOXY=0YlM{?7PRfs*~~FG7qZ%RRg}7hRPXM@W8}fR+hA zdk3z+ty9rhZF;*!iA~590*ocF-j7ymLM+d{+1Ni{gN0pfTcy(^mZ&#Q8A`srstjk` zZ~}XBd>h+Jj*Ift2>Ua>H%`IOTyLP#d6CRA=e><6wQgmBIW(e3a*gpbv$gC-mAqs9 z`ZbOue1hLR>&#vFGx?4)Ld()cjv)n>@3XF!dQP{zVTJq2&xcdrJV@yHXBJt)*0pre zap!$ZNswVsft6p(@V54QEH6Cy5b$YV4g_dcgi}V*WpyJKMaQmT#@9O=i6Mb*+U9tvh{)M$*%q%6eKY?G__WT5VFU_ zb6A`PFIdSX^&cn8C8JxJYK8l%&MxcVjuH_7h@;}VN5&B9C=Vp2u?1GKF{A3z1`S+{ zGPAdSKbZaamkub(6Pd=mugOKnU6yYQJaA}qfz_VS)Y`LyP0BoL$2Y%6P@1Pp?B3$)^dGRx{=UH6w>4kv$tquf`vZX`l%?kkT6u`s~Eo_lc>ryNWI zWsbGBkLGkJ{ndj44k*tnLUbo6&A#tr@Q4^wxG?C2>HVQum zjr(hsH|+tHy2k2h+6<6f?Fx`IbG80XD7vA>C!Rnf!e|34ew4oBYMJERRp(pbvp$D| zM<6sbM5!;K2-`CUT@)ID@dkEwcwO6ofmoeuy~WmsK#zav0HP5{ZA?6u2(gK!`oRr3 zu$m2-ocp$uNl_Zgqy$!@7K#%R0D&l1K!Nrp3Szdf&`5*EH_HDlTeN zo7>2~R1f`82eThmIDjFehi2_~eZkF0{12dR=IQ*F zjdIap^%Isr$K2S%Xj@O8??Z_mytQS2`Q>oRCphSpIwMBWwWqb3^NIkV!3S1)2p_NE zgsdK%burH|6ngQXfCK37tLEz9gUu*S$d;gCVibW@9oV=rwuN1h9)$Io-@XF0NTM;? z8WHhSJoj}#$ZcMVOsS&{LgFd7%#$?E z_-W)Bd~PIxT^};Hj>fvT^%hS)g!$q@{W+jjo+h*wd`}V`IsbD^0*Mv~k0qcYYdCw@ zweFOCxFnWgFoB(%ScZPRsPrw~!n*x^jK+RDpk1D)^;?$0MTgao*|iAd2#?00yPnZ$ zkItz5aM5^O7)==8&(00INhPbNdHp>j37?>8v(%e-cakDBwOU^VLCpie7s*z5Q)*4~ zN-~#HS{0!j3@5OB-*vIvv-&E}&*9*Xws`Gz7DC7e`#1JFk3?vIHjhMU=^}e#u3Y#h zm`f>je=JAg8B$R2MKtx$K%yQAK+rbnoK&c6gnY-jJdl`3aY*5qV{Q&VC|$ZJYg!Bm zG$M?t!jDlmFaz-<5&#+#u_QdR+cI?A?c^InlspJX5+S21O6R-`RMNKDq(T#gL4YZE zB7-!|28oVb@HJ&f9F7bT@pMw|PaCFu3r5>_OWg^_6~Ppqx$sv|blGN!bRtmUv1Gm2 z9@Rbc-koC9Pr0W|nG?hnJnPIm4V3D}>?#0B!lSV$&)`k(wk=!5sNbR2iY4g;Ta?(N zE|jKJ>rNC1|E+Af^90Q+!CdM;n~*IZ9B_rGxjaymYHzYgBt(G!&arp4@ONU82I@Vi zB3sC@1xzYdHbSz*8$E_<{J~Q><}c6xoTN(wcK4Jfv`kzP0l?3_dz6;vYCO*gP>Dyt zK_Zh>UC_DD1C>-kdseTDC1QaqcmjhoOc+B)9w1HZn5qa7Pa|c%JuY6%$j4FH@1%`lQi(O|a+@n);?ODC{@rYuC$6!&u&Y#ik zPJ>aU-TCihGSk2p!9r{3qBOZ&fzpI`W-GmE)p`|ksp4!}yNMwpfh{7Cuqu{Hgy=~T zh4}A$+k{;Ci_E1evUd@HEpB0AreFwl6+2_D2!HTYt~K^9{}d*vnzDH*aUccuh~>#91>=DrM6QgvC=%W!`JTaeXTA>{5Ug=+=_5?7T#$K?UqIO83Rs?DZU z>MsCnAyU}p+_w=S)-d4-bixB5e07mMtQ+^~6rJrr$3-i^zTS?Ki-~YkEPr_ehe^YDq-7<<~sNs=4iNF`UGLnQ4 zqE)fX^n*vCV20HF@1@M8XtIfgavk7{-x-(8ZFJn$wJfs5yq(90M8Y#V(w>;4sP3fL z6>@w*V(@5MqhSa&|4w(-7AHxdQ5~b3Z6PVL?!q6DCbWY1LR?G3gd-$B<>v(e2yy5# zf6_SR9g-r6PqkDRU|EY(}fy7Qq-|e7dj^@*Y(Ej6VvKRD-Pgg5D9}iwFQd-n4p+ z5@nKV(QR3ZJe_A^4F3If4~(ItQ8wq9CxS%ajAXVs_id8;x9v{A7-ojZYFP`a`)+w4 zAvlBQ>w+%bbB}i7Ce9$`p>(D0h3Y5l&>UyLxI`iK@4p`$aK^r-H5*&^x_*M=3=%`Y zb)#PQ+#{q5ZLGmJnzP_c?^xZSkE4*sz!_$S%vta=Nxch=i|6YAXYBf}y<16j-rf6u zm{^0CdSa$`@{N)?robBes=Ybr9-GjJ-J-o2Vf0xZ!-ry)48sX;&_86fMheN zZ|)J&B{uFz6&Ocw^^G0&S_J0EQG45=`W_mWEN%fXN2xpBO5fbpttzrdChoXwSKrvV zrt&O~Ibd?BvUP6Ou#3TIfH%apW_DW*v99GP)LzqlVGe#jjZcheg}DuLUKvSe4SS~E z@`IR|!%X8_^S^}DhLk^8bDjTuG)|jRuhnCl9fsqMY`L=))g(K@#C(}1w)Yi$t5a9c z{_-yX%)yHs5!E7l-x;0miB5q7d6Cywj~<`k4yeA!T=batpz6ncku6%yBLE0MzF>T& zRPX3na*yMVeN8OZ3^nP_c^&tYz`$Iv86@rj3!V%wQoYfikAL;DY2b4hKrc2XWpIbaT+DzN6g zjSL~?-Kp7wzQ7d3;1MU_?`{LgfICDSNpGBS^`zXxDJ4QC5uog@3$3AdCsi7An?E@INM;%HKS!yO z%a%^a7FTgD0+cqNvvng~Rl05~h&_0g(mjGotYs~yHri~B`vn}7H}ed^c2r?6ox(La z_6R=EX}D^zoz&fT7xKoOfEu}Kf0ZisjAnE31bdiOG3rv~nVj>?E5qr(3IeK{Cv5%| zQVm*tm0cBJk9FfSg{_#&4lwqvcn}bvGQWt0-$Iq6XRBO}Jwyxvy;4Xi*1Qy#wDB!) zP@QkQxf-(RWxR3x!N32teE+jDtaY!BnQ!nPj{}1I);@))U{9{rk4Nx_bFQuujNZ^k z{p4d1K;i{|P>qi7<$ygraCq$P(7;_b?|rPf&aR6HK$E9%tv#q9muAX648jYH!B&B> zBTUFv=|XEA2UNvjPNV{BoUm&l_(N1gsBbR=VedP=)8=a2Q{aHENP_U6sPT%OfIrBe zGXGdd{g@RyqfI#hlwPH2&Q(}h;*2>4iA1C6&Bpe}b@y2rNfQ_b7l1U0qmsrMSB}m9 z8q>uV1`#iLVg9fMKVodUNab0;0daB`4Np9znEkw$N$>~1{Ig>1ukSj8oE)1h?g;RQ2M#tqg4+gb-K$xnzQ7zr<8@9zgD>)0wqiTUHwPSp?E0>~2Sq8R zvZHY^l>6cf-~u%H1+(va#pJyADTQl#;1GjE0I|5ZV+0JWYO3~V@0|yM!Ikn!}?>nQ{<>|b);-KN*nc8d> z+fJ@Fz#=htR5xbNeSNWH%n4{}O1V}_zVwDTgv65Y8Ei-dX!{oaTdB~LT6JCogTR9m z0b0L>37JYM-%))p2OOe@`x=)hr8079w!*<>4?gBDSBN!E`A-sDR(nO2nv^S}P#Rz| zcv*#Lu_NZ#>=9f=9x%dY@yoY^CenNd@HG z_wn5XM$3{rM^}JNt+!V}@R^w@b5^;5$%+sqfYCA}_R_#)^Y8QrL7cYr&1FEWamEuU zSYWhZs=&T71up3p0Hg5~o_#nL5G`>>kAc;ax#pbLAs}~Yy3{eTS`yo6TJV5E4-rSh z0;|1S3!&cAEg)mX-X{&)nlt(#V> z0;!CqpwsDqKp`qsicGCk{<(LXt@cfwB2|)5XsLpmi5 zlAMbqWNC^(Aovk45EIHYHjzuenwKI&<_2|#w8vF4iI;@1w9Q?xz(FD6NR?`Da_TR8 zt#~@-Izu`l6_RWUE0U!!f`G6_6rZsoRbL+b3QfqD>p)*gY?2j;U@6@pV8nC8EaF^? zcxCk9+be&TUo6)X(m83Gv>Q9j)+^RjdWi1O#p{Wn;q*am!2}gn~BXcJ(I8aAKAl3%e zo&rnw)$Or)Z$mQ26uLm#Bt;ZSeH6>k^FXK}a<%8SS~t><8J+fCKnL4N?FIU;_#lL3 zEP@;H2+@f+R-I?|rz)I*_DRR2PSPV1w`K3V5i~>~VpntiwskGqagY8brshbyq(Bnw zD=m}16Jd(TKN!YR;H(=}tW5*RSsWNAD z1??=$lsZ8TktRv5TnTqv!RCVRjf0BYSD4FR>bAieCOm;oxMp^UR6(*ZSNu-IRYV11 zS6!0W<^kHWCuYlA0IiaKlCE5Zx#Q3X^v)rcZbn&t{uC1-Al z8z}6^W5;(My0YOuk&<6X(O@#(XXQbBO zFgINQ;x9xS;!saMsbR|5BB8N~>~{cp(jlph^jDFn=Bh&@NC;2F)}UH)b@dNy5%;Tq z)`1GZfr_h*5c4;kfzp%EAfsiq`5kFEU{CEgCn(A zH$G9=RKD-4pm6D=y4yFG&z&G(=N6*&2b2QF#&et6lk&wE5cRHw54u?-g1P@FL1of8X@wL}SSgtMZv#RZ5sX;Y2+s+qxyo$0r~lw8k(NopBxMrX z{BS=Izm`uSs?Vv->c;Gkh$_!Q`bD~F{)nI>?jhJ~rwwLBl)9mlz6DURABj!6S0uXm zzhDa~g z%=~rU2*Fj`DJ=*Vnxp1FnWy!*BL6@NBJqEv`7J^VQG$a#9pDAVuwgnRvrQ$ghg8g! z^}3k%=1!n<7iCP09iP-eBPa$QJOy_P&xI~0GBG1Q2c_i~9!KR403w%?@!PsK_>MTfO zB-bMGnJ$k0e-OC~qPvF}@u@BQ8Is6Xnm%Gj6FZeZ;7iv-s^IGCx|l|e{(qp{6)fTg zqI|Qw=n+ZRN!WEatwfLzf4uI^_|BI74#}4!ZkoA7gaX3y5Qytip5!VK{P zap*7oNIytsrl3P3$cTTRVD0>*AE~WC|8hlMcMUN!1P2ZraIiC|f3z+a$}mo8lr+9fE<#AJpyIN%J7 zf5f@FZP{zC5sA9?}AK+aiDgAKj325bDAVIQ{zLz6?}(a zB5Dx(&b7fLCJDdpdSqq?g7_1$>Qu0jeVVcNVzgeoEgG3QRCIZ_Bol?0nc&lJ&) zIC7&)e`%7bDfPUU@h0x;5Z#A#LSh%`5sChlT4iPkUWCJDxejUfFiE_o+vkBWM5OGN z%aB${DMd2edsFWvB1{m8i*c2I;A(_)oex5oierb09oA0WjEig;X^6xskU-%s*tGm4 z5v;v%j~%+VS(>UNBu*Bw?G~i%y&>6GPVI&JT@{e=x*^3y>!Eq_C2o*!>{WdmN}@rx~%Ib)b|79L{^X&xYt+OtzfG4i?FQ#Ql{P7M}s(xLH~9mL$i6wD7pZ%SL1(*0X}N&Q&JK z{H`rX;s!c*#uAabm@GI8QYy)^a2MKwC3Yy-@jQq0BaM= z3!#Q!BaXWj>6pYOslA#7@s~$D-lRdhMf)2T=<#t{Fu&y6#=%P0Z8Yg#k$5en=T1Pm zGbV_{sdm{tlM;Vm-7T;qun41Ly#3Cd1y{o)(<`L
- {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;