From 78224961d16e0900790ccb962819b177ead54ca9 Mon Sep 17 00:00:00 2001 From: Brandon Wees Date: Wed, 28 May 2025 15:57:36 -0500 Subject: [PATCH 01/50] feat(web): make google cast opt in (#18514) * add setting switch this isnt bound to anything yet * make google casting opt-in * doc updates * lint docs * remove unneeded translation items * update mobile openai defs * fix failing test we need to mock user prefs since CastButton uses it --- docs/docs/features/casting.md | 8 ++ docs/docs/features/img/gcast-enable.webp | Bin 0 -> 19770 bytes i18n/en.json | 3 + mobile/openapi/README.md | 2 + mobile/openapi/lib/api.dart | 2 + mobile/openapi/lib/api_client.dart | 4 + mobile/openapi/lib/model/cast_response.dart | 99 ++++++++++++++++ mobile/openapi/lib/model/cast_update.dart | 108 ++++++++++++++++++ .../model/user_preferences_response_dto.dart | 10 +- .../model/user_preferences_update_dto.dart | 19 ++- open-api/immich-openapi-specs.json | 27 +++++ open-api/typescript-sdk/src/fetch-client.ts | 8 ++ server/src/dtos/user-preferences.dto.ts | 15 +++ server/src/types.ts | 3 + server/src/utils/preferences.ts | 3 + .../asset-viewer/asset-viewer-nav-bar.spec.ts | 10 +- .../feature-settings.svelte | 14 +++ .../utils/cast/gcast-destination.svelte.ts | 8 ++ .../factories/preferences-factory.ts | 43 +++++++ 19 files changed, 383 insertions(+), 3 deletions(-) create mode 100644 docs/docs/features/img/gcast-enable.webp create mode 100644 mobile/openapi/lib/model/cast_response.dart create mode 100644 mobile/openapi/lib/model/cast_update.dart create mode 100644 web/src/test-data/factories/preferences-factory.ts diff --git a/docs/docs/features/casting.md b/docs/docs/features/casting.md index cc25e24da76..bca85cb28ce 100644 --- a/docs/docs/features/casting.md +++ b/docs/docs/features/casting.md @@ -2,6 +2,14 @@ Immich supports the Google's Cast protocol so that photos and videos can be cast to devices such as a Chromecast and a Nest Hub. This feature is considered experimental and has several important limitations listed below. Currently, this feature is only supported by the web client, support on Android and iOS is planned for the future. +## Enable Google Cast Support + +Google Cast support is disabled by default. The web UI uses Google-provided scripts and must retreive them from Google servers when the page loads. This is a privacy concern for some and is thus opt-in. + +You can enable Google Cast support through `Account Settings > Features > Cast > Google Cast` + + + ## Limitations To use casting with Immich, there are a few prerequisites: diff --git a/docs/docs/features/img/gcast-enable.webp b/docs/docs/features/img/gcast-enable.webp new file mode 100644 index 0000000000000000000000000000000000000000..f128b82e25c827eef25d99476c8f178c36e36c57 GIT binary patch literal 19770 zcmb@sV~{Rg(=OPyjoo(lZriqPYqxFNw!3%Rwr$(C?Y`%E&crtpbK*VU%*4$4QI&C3 zMPyZGUYTpHTuPE+Vob6?KeXyL*{1w=^gLqk{~ z!A-5cd@|p)mww9t1BlwsyocWV9g!@W)3ESU23j4TW3QvUBEM+>^!4kv_@!SD|2$tG zL8X30-XU++UuG;AbNM^~di%>j?BS?j65bep}ukZu3@_ zeycWE{*vGU;KB#SZ{i2IG=6h>H{TzQ z@+)|o{tg;J6b9rx(Y(1W`4s~czrBA}0Tkc6d-6Mg<{u8wtNy$GxzGHIuDRc-->PSTXV8QGb?;X1Fd*aS@YCul<8$=5wQV=sPx@!^ z-RlST3iPe?f&R*G6VMGP{<#M{p4q+UV&=I4I04dM@<)(si1)Ln`iK0@fJ}hT4=e!i zLqD9iG000)g!G9RPCcpGw01tVOy}kcH zQLhvL`Q_WU-5ewTzMOc}1`t;PxiRQtgw5I+-huJ;&vOmRoOnr z?9_Mu2WW_b)!LE82$KyTvMAj37GisEvkTWP$o9@=2fnkv)t&7>K(mt-J3ka+l_i_x z(FE5hnU@tpx`{qywy?${1Xt^A1mrd9Ip_aL+5e;eQN z2v>jJcNCPHh-?`(ibRm#$g${uVb1?DI#7XnYMfvlYO!l)YWHys21E+@Ub-6&}r1e}!V(XLj zH|=ZxI_S}r{&HvV#4jKPyYu_Cws&l`2qU$yit@YU4DoX#uw#~Z*=QMoUL!+F&*4Mr zkAN*RR$LwoG1|wk=IH3P5)cz+z$3zlG;T#kxYScm*B~jRU`jkv@56RBT?Xa}um^zO zFyb+eitK46J}d={xEl0^>ld}q6PDYyw)hwnItdoi{%U+7O{#g*5RabQ_kw=Bd(?l9 zKz*Tw>GfShTnq34ZTIo~fRl0D|RBsisUD9q^WAHQois z@t=76Kd9Wu4463Bj-!8#r;%kb-{|q1p9jJzZI?5Op6knr%L=YOpY!@}vW8VYpN7^= zIF<|;fb#DQYL3vt+1UOA8GOGFu;w|)VbpktPW3PXsm?|AA9HL2Zw|;^>ECBhjMrP& z^Q_ec^V&&Mdq{b^W(rb+eE3&>5|ip=#t9jLe2Vn&PeHq9n0)yc5h|)BQIi3n#C;KN zPh%^dOxO6V^P7Ukd9>xZj|zMttH-|4EkCq{zKUwUZ=?n6z2l<$S+^?S!znzFU zOH{wK-G$|4xhZ1W?nZ4;Z%7YuChoB9SBQs}L<-_L=F8GT0K$v+MH`OD@xH$&^ePYc zkdoS|>yQUi?4rfP*iuz~wUk|v^S~xg*D`pFknw*|K*j6Xqi5r*_5#kS;0}N>gSJPZ z=G}GUSf~pXhFUOE82+sS_0kfwQ3=Kb@GUqRe(odw&ztW#QTIQT({RvXe6yb0FD^Or zIctUxJ(27FPZv$~3AEm_T-!Zk<%XKK$8QTBx{&z<{&$Als+3H7!E7kz96`UO?-EAK z;uZZix>uv+oSeT^Z!)4Ug3y*k*xc<)$C2B)dJ$VsIIE!*&yL`hl%*dXYsq`W2}miT zR-Owd96@%O#;I7W)0N*EXTZ^sxh{sgb4|umSyS-!%X^g*D@Vo$A|5V$QA#noxOX&) zlq$`n%(QBFzIgZyt5+DDRg+KH2L%Z*95R0p$-wr8dr;-fDsvEz0&?SUjF%LmvkblQ zY_!K%6zzkk;D!QFyJyPRJ5g{^Cucrqn!bsEiF<6J5=99zylTC^>507izdQDFSqb%wsaLwN2^RBZBmaX z<+JI~5l3O+(`e}`fr(L`RNSBuTg-Rewk@92)U)2qO$r;OGQP0>TU;s6b<4JJd_&)Q zBPS7jxWeSzI+dhczSEXn@#vbq#YS!%|z++Wx_L{8Pw3Mn?#3H-z?(74_N!EMEKDsfT^A-pEU*q3F` zOP&|JF8CdBy5cp3tBU`hfg(aY+bQ_)fSWyDrhca$WvSk8t$-kpH{ldHnY52z=fN zcaV%aGF@=<3YE->Y7sO7w0Lm-q|B2!TTWe@Y?5hjUz}_Dj zg}~GIuU2$?SvL&@+4ya1bmGI`cx?T5@Y%03_F#}juE$^yaEIxcg*UC3*&Odj1Uju% z3fw@?<6@?pQ(&TPN1+e0sYepuOWfpVj--6?aTo7nCb~$>ZC){?3izZyqJy zV)|qsI=ZztDdt~;%L)cLw8`eDT`&BjhmTjB{BPq3o8dIFhrTotL_u3hL%`8DPcd58 zCN4+66A((Y7oD#=G}+dxfQ&_J2m2W7CIf8+dg6s8uMS}`OP%?lAyX<^qRdj#g{2av zj3jo|Y)znWH*6>Lvl_}b>sqb0pNFw1kV;bTpauM3xy%$uddO2EJode7G)cbX$KC2oobGoxf6Fjj-3y;|3J$;J?*kC}9%2hG&h8`&M}`RlPu zXsY?-^O2Hu(h=!tsO|&@NDxxNX;oHnUdk`V9I#%NJc<*T?ZnzYwOmKVdxcc*4d!D6 ztHoy#rABI6vlLB7R&^`_5maHW6fyP<6^18B_;>NdhWpX{SZ&9US5t2VY8$ zl1v%8!+}n2^&u?AX{f!?rFC{$_B9J@#qgBjxliL^YVzTND;1dT{Q5UsfW@Dff1A8r z5vl>mqgBawlxg4bGV z;BIgOet1t~jaPjJ88aL`4lnvJ*;s5Me!z_9y;QEn_o89%@1qXWMD6qecV?c#BihPB zB_kV74p~qHteSaeYZ+933`W0*pRX-Y5~6K zxNKUTWu2v}GL?a`nmP0-f%>Is%X+udRVNe~lTxzlTK$V9zfMzrxdR zgxd#cP6O`Hr-@1{z(a}zB8^gDFsHB{Ks8nlWdtw}yQ5*5U7U=_cz4KAy~V4JI%H#e zqnDh2Cmc8a0T&vTnrhG-uZP!_RhG8s@7WgFNxZ;>@ehWI2X9PJ+FLb)_2B&Si6YEd z4E544s_wW+2mrB-UysK-JB%K!&Bf%>LZP_W6+0R1jt(kD2;Bl55!T27Tpr6wk5d3i z4NK?E991EMnJP~zL{Y6pPZdZNTQ_wNkWL8q{o{IQPL|A$lA=opyi4?&P&&}S704K4 z-XgxKnEZd90s)2IVKHa@B!2WahVucILRC3R39$t)^@U1*)6$qGAOGl);9r9xMV`b7 zo1J$+Jn7XxfkPRT)!(PGw0UU2Ji`DXb~iG7ut4{>H@P@Y&2on@(n%rYJGM0wO97X{ z#(MKBr0>8GO7w5I{z3iA(*xIYOt=d1!OJP5m4NpNq>pKejZUnf(dMwjI<2Rn7Ns!R zN`z~)^jFZKceyhFO&8cs`(wPYtM|LR-5|%u*?X+`PROKBQr5O#{WXot;;k(G&F#*qkJL=G203LtRRSY} zO0GfDYWtziOj+haj6$##vF2(=ap2uceddEF;<-LzgtoLuzdSJ3h-nWb@(;~AeuE9K z?(FK#K+ZOM4+w_lDn`6ONndLUGJ*ANBd}NPG9Xh-Q_aI{DAy-^8vN8l=fR{0Mteg$ z5Hz7~!#BvJfA+C(k-73GdQk%QD^Mti6cRV?f^w0$=}bs!oMOUXAMQXbQskpF-U>z% zS2Ei!_QF=II}pvmr3HL(raMy9v=A@VtkO=!v$_5B`dt4$)1yiTnv414Ud%U|!M<#% zDM`Ii^q*rjhB7(c!lwF~g3g?k{UJC1#^fL>hlJRhmcMl4aeid`8^s&Q$-_0YI|)Ou z(qle+CIrs!{kNj~?qdLH-IhaKup9}%+FBkCAiz)nMhnj~B{v$)#({yFQqVY$dt#XG zfFvG?TLo$ihDc zntg)<#7mwkP9+_5%nA?f<2g4FauL>|*9;!T8oApnY-Q`g9qbh+N4PM0y+z&oO0^jU zT6cbx+Z83R?9#K!pFcEn`}=-6eMV;Rb2v5{v>u)E5PV3L@@??_tcT|`@^y!l;0Y$` zn&_A})O3s696Cj>tu$v+0v7d>}=l zm}1CVLCJ@b3U+a465bj1U=I-n>uPGeb2h>3_ ziB<%s-Xv09mo|YWlMW84NABfNtaSb-8W2{I-Ib4tkenEqu-qs@_Hn27?c2w4=KU49 zV?^yo&(5(8IU|k+e$spUnq@bZ9JN3;)Q0z14aXzWtf>cugrY!>>$?C zazBb!eDbZi=KcPCYt<$>GZaV)??hk~Qpr&lCj|m}bnC4rpFQV!psKZlkZ8jJzaICw z61+<^Osv)}7dp5Xi6l6HqMH__n54;j9UV#r*%g>cA>i*0-W$~}9TaaAK2k!cK~hYTc)f;~jQ zLyhiZb{CJvHvnY>=nY-XztDuE zwYdiM8?T0=^Sqg;`|F!ts(5xbBeQz4Vrf!z>D680LV_fc`D06$+@k@*``!?dp05nO zze+VM>XmmqcJwB}o}=ARC$^y^Ph7n9I?7ID2zmOV;awj7BJQtO_V^J?U@0HEd93I#05tNe#xa(iroNbz*6YIx*+{?;sk4R5p zN!2~2ps8qsHR&a3j7c!18jmU1iw^mwmVKLD5J(7Pd(OS$c$E>wa5uC-m(d1Vkl~EN z3B_p4Dp3MwVN8W{XEz1Mwb%_t8*2#^1vC&o2FL6cyS!k-7A)1=zr4f+-+oXq8<1T_ zufzrBK0mG|bNC(|O**JG zScHh^3A4)uzWG9+y=@njv6Wk!Ep9+Jy5NfgU+Db67yWTn1!NZ$7+fuTk8xqln+DNc z@FgnV%hOFI_`HKAujn;qhmmbq>(R_|u#qI)z|#b-5>r{iQT5E)?ZLF!mGYw^tvc}q zw0qWlmvBDN0z>K4b5=yq@#>2NG$=z~5SD8xa|PFJx8IwW_QoZ{`WiEu3mL>vCuI!T z_9dE%arIv*_Q_3_QITv`_iEG~=x6Iew2B40QPMp+o*1`eMx%XBof^=2@XF9g#g{V& zl*zLmD#KbYJSg+Pdt6t{waa}MQR_FI%*DV^QHvSc2k}^_Y}9FNkbPmjX~UkAt597G z$JTbL{X%(wBqvpm{vKs#rXuHJTxV*K1L|?REr+5l!}qAY3@2e!2={8lU24laC#!51 z{=yPfKA_gze8}T*h%^*c>SVgU)XfVgEa-ahd1`8ollljRt^wG+I#bFb{*=VtXXX=# zPf(*vf*1@)`2TH6qCzqbE@())Nkrdsxc$z5N;cdJz{m$F z$IWM~Ew6TR0u^9#=B$GbUmp{?Y!9#?af5xjGFY&r5fLke=Yzo*9d3M4q+lO~gm4KH z-Zbo~MtB!nJfmRHO%cwecE&0J^J7Eg01)G-G81=1qGgoXy?u_0w((V|t5)^qW^u?6 zkeuURH&oxZT;r2n?Ctv8@8Q(ls-h%(Ip3-R@J=z$T56`kWEK39dtEN0 z(BeHcdVisJ?=l@GyT9f(W{g?YbL{pK$zJErimxiPVD)l%D6o>`*{^FM4oD^|ISaYx zP(?DJu6XIa>^uYrmo7vr)FU)Gh-JW!?}C`cB|@tx{Oi8Bq!y`Ctf|@}2KTS!+T+27 zTZumw^#ZbvR-}co#pW_!JYwP44Z&?)cyyI4AT{)q_SvY7h7z)rX^S-WhMy75 zU`&3>*L}e21Cgc}X{bg~(2gK^NdT~mIC2mljfjMH1P2~r{~A@m^2t zd{$4T4LemevSp+XIkz^_$!IN43TL(;oKB4#yaClslCn6K=1TdPrgG83mh&U zVFkVmXg%5>`cfH#wy!dJIhbZJifobn?I{_DZaHQq{RPJ}{C~$MOiFa-j{tMdcGQb_O3H(b#3sQ&N=#=jBFvY{oz>v zs)j~)i5%^A+BLU5-VAaSaSygi@7(5TGbTYr2G#||xRodN>P7Ha*oW`CO3H9Rltu?W z8js>A15-S+xu?xryqzIvvT+hc%=shV}D12rx62OLyNY9 z2xb$KfSoPiy5?M(`hJd`M&qqU~nM zEd7Q#ER{Ozvp^I7bk=*RNJlnXwN(Je(-0OVaRh$1AtPSCO3`DGmp#fCIfX+^=-7BW!ENp5G-JajM@b@nWIeAFVrtI3gKv0cOo7ydT5BDyTk=vzh zx*B=ICLlfFXx41H&Uo2v=KTN;1j3vZbrC_Az5y|iesK=wBV!xz?$je+lD>xlX9?zC zN;Sjm1^n1!6>#&|!TxA)Vow{D3r<~q2= z!=?}*UF{O-OTmWN6D&orE_O{)E{;L>4E8w=Uwr8Q4z72(!I zu9|K~?lw532KUG114YsFJ}1mG&$P^~ZS2a9da3Sx8JLuWHDsoOU=(RC&Uyn6f@8B| zZ2U9wKDOAKW(}C%A5;b372G36v?m5Wm3#Tc7Zt?A>ss;WIvuU%2Nw^tSA{}%J| z&+{{kq;x!^Hjb1myk?`ZwD$GyyI~xBcwM}cA+^TrRR#(w^q@f zcdJh-;nr(USufFlJ6}_r<_qHY!;5)=YX;#wQg$EM&iF&&$RYKfk6Dh2aFslgt5$g~ z&15^LS+4uH#=q~&eCaTQFxn)Wm6`9CGKULNo{TQal&Ney^>H77@YgEOz?gW$vGtc< z_(;2X;lvu*o)@0f$-U{@+*X+U1OffhJ7H*kCIf859vk90KJ%xID%DNUeqnoJug9EWCTrApU@t{PvaBaVKL0VYI36-f7Qf{|9koBr& z<#8cd3uTWxa8-Y!x`U6P|DIjK3T5s};1DAI)ze~MmAmyod1D2rPGG`A0k={T=pW)Y z3|DKeDBycyN}bcxULFT5a6Q|wQeq9ddQ_7?t1f`A))$|-owLN{Gavd?q*J)e4fE!V z&;G=npJ0C=oP6SlqpT`Uu1o2ytn=?Ox;t$lGiRyk-TkV!CIo$o9fz)3^IOC_na|~F zMb1YHwk`IHNSZ3{TMq}uk*S?ZBzkV98)+iwS(f>x#KOf=KY|*9IkqPMDX`Un^pDJg zZamW35xS(O1q{rpHbkEf)U$yxS-jZPKJ|_cuLhK8pQ2DJ1P^~qOwOra0;_nvoJQjs zkTko7gMC)L_#&Z;Xgn7yJG0`dh+9ZzL)vDCP)Z^f+|saZ*c1t=fm=OKW{F8{ir4`r z*HN&$d}nT@&jK%Pq?wQ8fi3UsN4%pw2@m;X+-KU$B45`2%7Y(mH|e=eeLmIcslCy~ zZ9xI?&AdvFCE1yw6&@r)&lOH+NibM%u)a-_csgV~C&w$_P8$z#2(}Br`^=23Uuu|K z&n%`y#}OUuZiD*ax7K7FS0FtFx%iHOwa{|Uevoo4q*Keuu19VAOyZQh?TQ^OdP`3; zZuV7(BAo+7hikgHNQ~(-DL&2x4Y0G1@M63T39*-d%X7~1XPI&KnnbZyk_(A=M(QLw zGfa@H9C*0p_1#zP=16KqEAb-{QrsrgGdoZq&fiP={FqIA1hol(CN+xrc^bYi=D!=o z8}oc0uW|W)UAcoFoOJLpGcTXV3w^b1aw~{`+?M-OibsCvteb6HwjjT+uE?aCj;$mF zRK?_a>FpvxwV$lSbGp@?Ww2ka(}j`x#3*al=`3>^BA(tDQ$Xgsq{R%z@ZhK`(9lC6 zQI~ApTkD@Q17#*F8@odhuYBxwk6or`rVkW6_a(_DUNTj$r+bTT&KP>b(@G5dM(~{* zIZ6S!O}-6HTuFR&a!MrVFhw(JTVvmQjqUst_$LdHI5z!$*^9mTK9nw!CMdt=Iq(UQ zA?_Hlpl;1_8!FZN+)w$7?gWyip^lhu9YLc5gEFD8b zcpbOtJ1K!%m)jcK2+h`KDdb^90cL%FwVs%LY5)g*zuy0TwW_w_-SXZA>5sz(ES!$Y za&T}C;le9GZ?CpXF-Qi6xv!wlQfM7tPhE|`qHwM6L4%FWv znl*!x6Kjj~9Y96Ck^6m%`;oK8Kr_W#AA$5=Qk_SvNVD_mABC`eGK%Yi5bWO~eR!GZ zA^}mKyY-AHXMDF9FWEzls1BIFX25>%&P9IBuuGibQ}F2rntrWo~D2yn%749(~WMQ9o3m2%e1?(Tkzt5IdT@v}O6 zgVKCRzq+P^l-pu7Afm6U=~#FjlIxv)=vmi!g6}%-?s%tmvSB<7oQBd}+LXz=3Zn6i5XA8iG$ewQv0 zO7XmmWF$f&)6Q|Yo|0qB79*wL<##i#==$D%i^qtniBPGfvi0PpfqGbbVE%W2$+1Wj z)Mm_sEf~X?9f2;rs~n>h;Vk!LP-avD-s0x&;&({J$^^AkTJSd6a6u}FO`u$gtJ3A! z0SfZ4<+Z=p1721e7pV1h3@}UFZxr(G)I4MAFS{)XJ2kVN0XJK}O9Eh;j^(0ZXgX-t zBF4k)L2Yv8tHCb}C4V|_)IX8s-4vo%ZWee}H^7g1!5Vwka}3)rri8d0M=k3T@%^|* z28(4&a|mEBhHkBB{t|BOr)t=G6h5(o7)us4)*tGlJjS6>=hWK)UR6*C<%(9CT`Abd zB*3yMqLDkfvi$3H({T)2MvxHMEc?c*PeCendsU!|plolvjY5+|*el^Ccnt0Vt@9J^ z{?;$QPXu&aMnZ6$|H+!LL-{LB5q(5dFuv{0vGL8x6Xp!XSZapHM0uODTSRi9T6}wr z^W3{XtDF4@(A(@EE`Y5ZiaQ0XX@C^_XEo=#^@cXpY6gF!H=q#N)7)%VXI0)at@)zq1J=J+Zsh^hOu} z(ClyGJ-X2QUhOC)N(mO}b$;eo?j?+E3B;05^u(-00z8LR|H<%rq*!d*xDZ~W(xuAT z$?o#41ImtI+M9uVV6Ysur6DKYmjp&jt)6JdKmnb&)?&zn?Ud9fh~9XU&HCtibGsz( zz3N@jX5qDRyRYB6HUARJ90i{{BdWe`?LNx&5I_|x>OoVo{&&lextILWobxvw``f_B zhgv3JFVByT?4;@O?mk!dUZKelf^I*fc!U|lATIk1!4@(${PZGN`zQyHyS1|8be8I4 zJW-u?xk>5Mt=UF-83)T;9-=kG-~a7x>ve)eReZ##Foq?^SnJzk*0l$;%r%a*$h=N$ z6vTv7Tw^Av5WnH|4q6{<19>J>7NO!bkdZ5g03Um=8P+M14{1$@yd5k(KjSwztP-{_ zYjh~^AwW9xW{k+mSfqb!O2$yl;;v9f;HeXy1wg_m1ShAMeWJzoC zSBtWFR?WrN${%DRX}gD2TMrPWX7}uBWQF`T#5RH-#aJuy@;D_8e{~S#%!C{jC8m0+ zASp(J^W4iJp*P7gi^}~e6zW;SQi?8z#XUFBJX&2ES$B*FiR&krdye6F^|q88L0%o1bC8IAeHM^JuN9_Asw^K3F{;&bb%Y3@*`v@7#g4g$j{@ZrSS%C1E$Xw{gtI z=7|>2FX|ZD-u2>HQ}&Z83$v_V-B;^TQHh_UuR#oXKJsM3i4z*UQYPoOPWL($Mw~ca zUxBj5KJRJB5=&zGv+CSpvYE#DUKZCvP{`?CNKu>f+zkZhBq7uwZ285s;p8^{ie=Rk z>d5T$BWwlyYIg!-p{nlPcwvf?3`%UzDjczxms%1POdc6=`I%g;yXhKB_YCbK%B%8Z7c3$d8=(9BiQ*5x zwz`BW$?Q`zBqL-R?|oN0&e_-x?{!n6g%WVCLmO`5e%XiauI5eN&w6f-3{mwZGVM*a$TKe%cm>#t&5*A!c~BM5`dNd8;?ue(I1AE5Rd}uUc^1@#LWzbJl?f<} z-F;^H>m~U=hc;Q44WC4KL8ueREHUK&D5~+4oKo4~jO@-8;5mPgwc$X_F2u%}U|X~P zu7>pWr$o3YYl1P3D5dpK?afLvKARXlkuPIkl^d;l*x}mEGQ-h{o5lYdF)?MNq}8Hz zL=da?bqSF{8lOT^smLS7`$KluVFNRBPER6L##;j8{= z)?XA4${0_E>$`A45Jn8wY_^@CyGQ7;z!jPG*+|0x7mP)@jmGkd#ZC5P4mmKQW7{<3 zE3w#uZ(V{Y$j!I=bkz~Ojca$ZLt!ewSG9$a3@HX-GQeBNx3I|TL`~2p;NVa05aj#I zjTyMTk2w0zov0MFw6 z*Hw?*h#LK|Sa5jhbQU`b@h8k8ApX`bs*b0k1n0RnreWA#*#L)5rK1~Hb^#yQ z#=^Y+>M+|`#io($Aj?xPUn(*xM))QDF0?6H0YuLOw43vHS~b=ktNiQ? z=EEGnmDKe=D+o>Jwv(00Z}y!qK?&&Ds{C^0I1_WGig)YUxIKOY_Mphzrk+@sB5i2i znT&T zTB^NuWWhWp65nrEyyn1w9xtPxT9AaAw8rX%khknY4tMM2OFv7TZMJKeiLha)4dOng ziBLEi<~=n;Z}c$cC)&BC*JNHirQIOgu^sAgKwpB3u4_6hECWb`Ym#GTL#${2F4ej@ zd(TY01K)M}S*|iG<#LNB3X>;emabp`ULF~pag!#cxP#xWo_%ltva*|6r~C9%*NLJD z>hFt@(vCTH7BA9Z$?o~f6q;%KcGtd%g%JcwbNSVq=Y%E(24i|+hr>6~_==y2h!Q4v z&)|w6zWJosdb<;~;CEhoBc>)3selNuG~dg?BV7k`BV&uiR^SOgPxK{x#-BbKC45I= z$1Rp$u8ZqpR50Hb%8TXg#hMf(#}%YjbrMupJ@D^&o8mNZw!eReYaed3+3sU!I!F}#R{T{ic#6pZDRa+1N2MxV{-UsmbgqPdL5`7BjYZwE2NRFsr=iB6agA2XC znEZKS56-e^jBX~;sG}@u2o0{kdt2RgEVRql=4OZJ&KUwQ&3!aQ9f(3_%na&}H-(o4AJTm`-kjPN!ev9MkU4>XhfuM{ed{~`12Pp7Uoe_cwSIZjVc6| zJ~P9>*~*yZ^X& zslct-td}J$P7kf2@3vi8EO1w_4nLJsSiB}z`V^Jx)<)0P(IC{xv3PWTiH91?UKjYJ zeU=`&(5QDhq)yqq+(!~L+KXRSgD(1s|72Y&yF{QGQAf|+^U5~&n8r)EQu{mwGCpTr z8&!8tM*yO_ZK_9+il_gyXyq3vwzE@I!m+=Qun1*zjn3ewt5^O{eqD(xR>=&Fkj{AT zs2E*)0dtFs+l}mO;Aft@x3(i=9oUlm@t4ZFdl zdROR0lr;IgrL^!l!GzX8FyF_O^r2b6(8U^R;_TCDK^NBMN&$=0k7AzI)6zYE}&y z<2#^2Zj91v<9ctx!G&CrOIma-ekS|Vv6wM)GK@?4a>2#mo&f8pyXKu0k z;^Z(^YO+U#KKO8TcsVOvR*V=q_vHkk;zNtXkn|J@r6xsL#?Le!^BhYKJ?G!Rm%X;; zCVv&~j}KaAtn)IDM&J)Zy=2(?l-)uPL{g?WuRCVqcjswNdq(I8@?{fKnS@&6hGYW$ zty+e(-=NAx9NFsMo414`xuJViLZh2X#eRCiC{_Ze_q;50yNxGfq>qkmz7>ZZ%y@3< zRpheAZlI}inT0n0vSfNxLkouPr9bFXz&wg38`9uHzzmt#-nocIp1_o+o@(#q?aB>w z(2}Rk7yHrcT+POHe=$@4#hctf9KV3uRYwpBub`^Dd`$#f){v3C-=f+L@z{r1_UZ@_ z%CHKn?$X0mFz|pW&~>X9Pt2Ge7@aixJUff0M7QNQth1odKUSV?liLxLc9u2(8clj~ zyR}`kOM778Ii^K=c zltj@SA|Ll6nC-FdM!H1ZA?4%`Zw-Nb|^f$>3VtfHL(y`n~p=M9YZLR=D`f@%3awe zj0uKT5)vQ9hGPlX`-pXzvx>jL$YsJ*`neGXla)S#xNFcQ&FhF^nGux_e*L*HHjmjE z|1NE1`$<$p8Xvd&@LK4Ow4WpNtH|=47^}veQ}ak<(;v?^A6p=O2HQifmFHPBBuWpR zzK6P0{;2r3{JBXy{+GW0Y2(E=l?56qUupu~98qf{*j7;8OU|dV*yWtdFKU9I%zq9J zu7-EVZ_JwMC99W__5p*#qC5;cg;PYvquXx~KN3I7K3G?>a?yckwX@?OR*Gv!Twl7A z@~W{$f-SoB;_7#*Kzs5zSJ5QGx=a}xt#t)Q_fd<+P+g+;;c3+G)cAucvlIkVm5xJ} zODdrap`$FGF|80`uVFl2-ATh>q$8ZDTYVd>5W(cZ=`X>xGjbsejtKseKWz~#zWUJw z{$wu;LDtX&0x5FUb^4(ez+_xP2~0E0WD%Jw*qhEbfbpJZ@1HFSuV&4$gFl9^?BriIas2f+jS$K1PExk*kcyjp ztM_FJj3^q6nP7Tr9Ln9E(CR@F2f>O_8*RzXD*#R%Qi%~o(L6u)&9A`QAt4D#?`$w{ z+}l4snXws{Uq*ta-r@9cOxZs8R9UaX2+t0*@hkkn3B>`2w5!@&b|M3>c$87p6%IY& z$WNV?a&h0DO$C<;h4bb`+d1&4PdkGTACOFf9*i4_;)WoNNO-yAMyinP-&caxh;RwquiR`6n(BT9u+ zsY0cL+Ym;51mm-8y>517I`u8GJBden>m14K>C&|5ZYEGT;TDI7S|MS;l`P;0!N8J^ zQjJ(voY2Fo)#?_eUY#LxucnkpP9{t&d3k5@_Up4Tdo!6$`(->ZuQq#;rRp%>RUoR>avDidRFPG3T`6ei=SO zb8zz-atHEKhj`BrA!CZ&sr?3 z4`VST`JVqFayv%fIs!+jlJ!kS1L8T9$O-!GSkK7*Y^KH7XLzq3aiF4FQNb&Y`>jhX z%9khPGnXjW{$i&dHKp&ia4;^i$BJdnm+M+vCK!CBk@UY+Z=K(4D112F5~D_4yVS^~ z#Asua1hrL**50$!dSla)7_lNz)GAf0QhQygQmUmwPncijJgLMYFykYF+;R5C`p=Y$p6yN!qmWvFL&MFI-nYR*6nJsZ!p`G`+evn zFk4C&pHK3$x}#(4^U2isS9=3YZk7uIH+AE~wI<`i7>r1U%W9zlc&9+j+{;^GDce+x zu|Fpfl4gZ%{@cFlJieZtaBXA0#{1>7Py}o&rnt?-S*6z%Z?(`mQKqcOXlYNbQtD%T znujP1)6wO=6kR&5nwSNPKxT-0@}G7}B}xS>6rfCI2AE7FIOa zL85?$hB9*0;87oG$f~lBOKa;<5n>luE0JOl*?ullJTe~#+49aQG0j32flR6;a9r8y zNs2Z24O@MqIOlk6+>+YCV?&p;G0u-FNqUihl#QiFpMU^xPo7g1HtI(0s2oZjin2V{ z%tAh7CcG2FKkAKsa(VCeO|w9fuJ~f&lM8geqL63b*O5NUDn?@T81-trHL~4tGiruA zN$%_Um<9Ueklk?p*hfkI(@2~aEPQJuOnlFD+Pm{d%{=^~cktQwDmPRM7IoH?Qoef8@@ zW-!o^VC>sUSAOWKR{70NBf066S7F}d@{tV`BR9oq3th%R7g5T0BJOdiG+kl#C>1OX zdQIOHcYibCw;kAuH`rV|s51JYKF6$7N3fuWA0X!Fh|E^tUxOH_ZL+LR6BUXI8%$(j zz}vy?QvJUze0e;V~m<>5Z%uRyP4yk`?xt(vM26YLl;6@*|mnlPJ843dPM(Y1POu1!tY%jy=QGRbQ zPeK`R{FI$HC$6>iCueu1+7Ta`wp{GH)%(IHL_odA^6VRa<*@7<|2+aK-r|QC&ivqa^Zqi3XP@qSjZ&ugFo{GJh6IfPg7I;IxOX$ zJ7y4>NOpRFrnTTGs0hQf;C)k$Rc*ayTQ)4WnK}OoUsR~Tz;&)g`L1P8{#mP$1R19sigy&x;uFTeM_FjPdWLvaxG>37omI2zKqD zZ^?D9ZA$mcj_9pc5>Khm4QyRuL2)!7nfb<}y(cV#KdGFGWdjHtBtQvnhObJm1;p&w zgWA7&9c{lPBK*SO@jSYtlnh)0_ij(qA-LiyXP)Mc=e5(vIwHB1Nm!<38+Qz@zgup% zLuIT9m+oWr6eDzr=r{F6okK&4x~jb(8Z6wT>mT6Q0ZchtN6uod-`HT6+OL9{#fS>I zX7!^r6HH+6+LutsM}&9iFPdthaqFYkcLli4bg^qj3&=j5*O?{ zgd&-Vm5vKO(_2Xi@%Ky~;Ob<0my>X_qfsniir%_=WsM|q`_SaK@cN_oE1Z9c9Eo;2 zG;*g03LjdQTa2U~i#Zr&7J*fgx;{KGSNaSlGss^GBYfRoxc-5??=r^Mp{S|;+Fe6; z?(rs?TP2G;4Io2RD|u%?Yu(XWqhx{)2zsj)uckb*E5C zyK*I7K}ntmP9&xKEx~D%Y;1qe?%C>V(OMI@bo;hAfi+{k^D+*(d8_5rqO(ptlzNiw4-4&n@H%~( zA|$w!>XB(Q*8r0UR4D5Qqon+1!d+qSA*cS^YK}8La7Rw6v!x_FiBFX;SX( z$?W*AZroIaKuCxrRTsbV%P2(an1Bkv${aB`uqroW2u!|BmFpY9|r@=B1EAR}P{1tbaKSje+{)>W2Xp#$}m8 zUgoID7YVz`4z_gGn4H_T85`pQLO6Uuth532B1oQrmH={-w|~zcn6i$H7pqD%{WxJE zVL`F^%*b{(z3Vt7qJIU9Mze*uE|>tw0T^3F(72Q}K-u@>YC;tVn(Kj6FjCwM?%}uRS!Db3ztS!#Px-T(}GXABd}`)On6(B7w)Ai3>Ki& zW2@pw;3u;Ok*ULMdTX_U9-F+r+hZK3c1-oNTycUK|0!6vxawR29lNS*oG62f$#+;x zF)Tw7(^KWswdhdEWyPwTBhuPMIa@m0w z!v1g2wx0w2%W!RA#|;-J%f&{x?nmdpO6OJ?&c_?khtfYuy30u_iLu2uen_zfElNWZ n?vEoN2Aeft&FdW#xC*I8Wc|xR0~^$Hj%j2qrt`nI{onWp+F)dY literal 0 HcmV?d00001 diff --git a/i18n/en.json b/i18n/en.json index 56e38cf8166..603eb58b3db 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -604,6 +604,7 @@ "cannot_undo_this_action": "You cannot undo this action!", "cannot_update_the_description": "Cannot update the description", "cast": "Cast", + "cast_description": "Configure available cast destinations", "change_date": "Change date", "change_description": "Change description", "change_display_order": "Change display order", @@ -1027,6 +1028,8 @@ "folders": "Folders", "folders_feature_description": "Browsing the folder view for the photos and videos on the file system", "forward": "Forward", + "gcast_enabled": "Google Cast", + "gcast_enabled_description": "This feature loads external resources from Google in order to work.", "general": "General", "get_help": "Get Help", "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 24faee28ade..b8bcbcebcd7 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -321,6 +321,8 @@ Class | Method | HTTP request | Description - [BulkIdsDto](doc//BulkIdsDto.md) - [CLIPConfig](doc//CLIPConfig.md) - [CQMode](doc//CQMode.md) + - [CastResponse](doc//CastResponse.md) + - [CastUpdate](doc//CastUpdate.md) - [ChangePasswordDto](doc//ChangePasswordDto.md) - [CheckExistingAssetsDto](doc//CheckExistingAssetsDto.md) - [CheckExistingAssetsResponseDto](doc//CheckExistingAssetsResponseDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index d3a342db6c1..ca33a5dea1b 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -114,6 +114,8 @@ part 'model/bulk_id_response_dto.dart'; part 'model/bulk_ids_dto.dart'; part 'model/clip_config.dart'; part 'model/cq_mode.dart'; +part 'model/cast_response.dart'; +part 'model/cast_update.dart'; part 'model/change_password_dto.dart'; part 'model/check_existing_assets_dto.dart'; part 'model/check_existing_assets_response_dto.dart'; diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index cc01fd2c064..1059655323a 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -284,6 +284,10 @@ class ApiClient { return CLIPConfig.fromJson(value); case 'CQMode': return CQModeTypeTransformer().decode(value); + case 'CastResponse': + return CastResponse.fromJson(value); + case 'CastUpdate': + return CastUpdate.fromJson(value); case 'ChangePasswordDto': return ChangePasswordDto.fromJson(value); case 'CheckExistingAssetsDto': diff --git a/mobile/openapi/lib/model/cast_response.dart b/mobile/openapi/lib/model/cast_response.dart new file mode 100644 index 00000000000..d49f1ad3d76 --- /dev/null +++ b/mobile/openapi/lib/model/cast_response.dart @@ -0,0 +1,99 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class CastResponse { + /// Returns a new [CastResponse] instance. + CastResponse({ + this.gCastEnabled = false, + }); + + bool gCastEnabled; + + @override + bool operator ==(Object other) => identical(this, other) || other is CastResponse && + other.gCastEnabled == gCastEnabled; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (gCastEnabled.hashCode); + + @override + String toString() => 'CastResponse[gCastEnabled=$gCastEnabled]'; + + Map toJson() { + final json = {}; + json[r'gCastEnabled'] = this.gCastEnabled; + return json; + } + + /// Returns a new [CastResponse] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static CastResponse? fromJson(dynamic value) { + upgradeDto(value, "CastResponse"); + if (value is Map) { + final json = value.cast(); + + return CastResponse( + gCastEnabled: mapValueOfType(json, r'gCastEnabled')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = CastResponse.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = CastResponse.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of CastResponse-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = CastResponse.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'gCastEnabled', + }; +} + diff --git a/mobile/openapi/lib/model/cast_update.dart b/mobile/openapi/lib/model/cast_update.dart new file mode 100644 index 00000000000..87076391325 --- /dev/null +++ b/mobile/openapi/lib/model/cast_update.dart @@ -0,0 +1,108 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class CastUpdate { + /// Returns a new [CastUpdate] instance. + CastUpdate({ + this.gCastEnabled, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? gCastEnabled; + + @override + bool operator ==(Object other) => identical(this, other) || other is CastUpdate && + other.gCastEnabled == gCastEnabled; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (gCastEnabled == null ? 0 : gCastEnabled!.hashCode); + + @override + String toString() => 'CastUpdate[gCastEnabled=$gCastEnabled]'; + + Map toJson() { + final json = {}; + if (this.gCastEnabled != null) { + json[r'gCastEnabled'] = this.gCastEnabled; + } else { + // json[r'gCastEnabled'] = null; + } + return json; + } + + /// Returns a new [CastUpdate] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static CastUpdate? fromJson(dynamic value) { + upgradeDto(value, "CastUpdate"); + if (value is Map) { + final json = value.cast(); + + return CastUpdate( + gCastEnabled: mapValueOfType(json, r'gCastEnabled'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = CastUpdate.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = CastUpdate.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of CastUpdate-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = CastUpdate.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/user_preferences_response_dto.dart b/mobile/openapi/lib/model/user_preferences_response_dto.dart index 215e691cb1e..c729e0d80fd 100644 --- a/mobile/openapi/lib/model/user_preferences_response_dto.dart +++ b/mobile/openapi/lib/model/user_preferences_response_dto.dart @@ -13,6 +13,7 @@ part of openapi.api; class UserPreferencesResponseDto { /// Returns a new [UserPreferencesResponseDto] instance. UserPreferencesResponseDto({ + required this.cast, required this.download, required this.emailNotifications, required this.folders, @@ -24,6 +25,8 @@ class UserPreferencesResponseDto { required this.tags, }); + CastResponse cast; + DownloadResponse download; EmailNotificationsResponse emailNotifications; @@ -44,6 +47,7 @@ class UserPreferencesResponseDto { @override bool operator ==(Object other) => identical(this, other) || other is UserPreferencesResponseDto && + other.cast == cast && other.download == download && other.emailNotifications == emailNotifications && other.folders == folders && @@ -57,6 +61,7 @@ class UserPreferencesResponseDto { @override int get hashCode => // ignore: unnecessary_parenthesis + (cast.hashCode) + (download.hashCode) + (emailNotifications.hashCode) + (folders.hashCode) + @@ -68,10 +73,11 @@ class UserPreferencesResponseDto { (tags.hashCode); @override - String toString() => 'UserPreferencesResponseDto[download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]'; + String toString() => 'UserPreferencesResponseDto[cast=$cast, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]'; Map toJson() { final json = {}; + json[r'cast'] = this.cast; json[r'download'] = this.download; json[r'emailNotifications'] = this.emailNotifications; json[r'folders'] = this.folders; @@ -93,6 +99,7 @@ class UserPreferencesResponseDto { final json = value.cast(); return UserPreferencesResponseDto( + cast: CastResponse.fromJson(json[r'cast'])!, download: DownloadResponse.fromJson(json[r'download'])!, emailNotifications: EmailNotificationsResponse.fromJson(json[r'emailNotifications'])!, folders: FoldersResponse.fromJson(json[r'folders'])!, @@ -149,6 +156,7 @@ class UserPreferencesResponseDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { + 'cast', 'download', 'emailNotifications', 'folders', diff --git a/mobile/openapi/lib/model/user_preferences_update_dto.dart b/mobile/openapi/lib/model/user_preferences_update_dto.dart index 3e420df119a..73e3cac9ffd 100644 --- a/mobile/openapi/lib/model/user_preferences_update_dto.dart +++ b/mobile/openapi/lib/model/user_preferences_update_dto.dart @@ -14,6 +14,7 @@ class UserPreferencesUpdateDto { /// Returns a new [UserPreferencesUpdateDto] instance. UserPreferencesUpdateDto({ this.avatar, + this.cast, this.download, this.emailNotifications, this.folders, @@ -33,6 +34,14 @@ class UserPreferencesUpdateDto { /// AvatarUpdate? avatar; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + CastUpdate? cast; + /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -108,6 +117,7 @@ class UserPreferencesUpdateDto { @override bool operator ==(Object other) => identical(this, other) || other is UserPreferencesUpdateDto && other.avatar == avatar && + other.cast == cast && other.download == download && other.emailNotifications == emailNotifications && other.folders == folders && @@ -122,6 +132,7 @@ class UserPreferencesUpdateDto { int get hashCode => // ignore: unnecessary_parenthesis (avatar == null ? 0 : avatar!.hashCode) + + (cast == null ? 0 : cast!.hashCode) + (download == null ? 0 : download!.hashCode) + (emailNotifications == null ? 0 : emailNotifications!.hashCode) + (folders == null ? 0 : folders!.hashCode) + @@ -133,7 +144,7 @@ class UserPreferencesUpdateDto { (tags == null ? 0 : tags!.hashCode); @override - String toString() => 'UserPreferencesUpdateDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]'; + String toString() => 'UserPreferencesUpdateDto[avatar=$avatar, cast=$cast, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]'; Map toJson() { final json = {}; @@ -142,6 +153,11 @@ class UserPreferencesUpdateDto { } else { // json[r'avatar'] = null; } + if (this.cast != null) { + json[r'cast'] = this.cast; + } else { + // json[r'cast'] = null; + } if (this.download != null) { json[r'download'] = this.download; } else { @@ -200,6 +216,7 @@ class UserPreferencesUpdateDto { return UserPreferencesUpdateDto( avatar: AvatarUpdate.fromJson(json[r'avatar']), + cast: CastUpdate.fromJson(json[r'cast']), download: DownloadUpdate.fromJson(json[r'download']), emailNotifications: EmailNotificationsUpdate.fromJson(json[r'emailNotifications']), folders: FoldersUpdate.fromJson(json[r'folders']), diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 98382a382ca..6aca3c349ad 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -9556,6 +9556,26 @@ ], "type": "string" }, + "CastResponse": { + "properties": { + "gCastEnabled": { + "default": false, + "type": "boolean" + } + }, + "required": [ + "gCastEnabled" + ], + "type": "object" + }, + "CastUpdate": { + "properties": { + "gCastEnabled": { + "type": "boolean" + } + }, + "type": "object" + }, "ChangePasswordDto": { "properties": { "newPassword": { @@ -14806,6 +14826,9 @@ }, "UserPreferencesResponseDto": { "properties": { + "cast": { + "$ref": "#/components/schemas/CastResponse" + }, "download": { "$ref": "#/components/schemas/DownloadResponse" }, @@ -14835,6 +14858,7 @@ } }, "required": [ + "cast", "download", "emailNotifications", "folders", @@ -14852,6 +14876,9 @@ "avatar": { "$ref": "#/components/schemas/AvatarUpdate" }, + "cast": { + "$ref": "#/components/schemas/CastUpdate" + }, "download": { "$ref": "#/components/schemas/DownloadUpdate" }, diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 0ce6f417b15..050fabca927 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -128,6 +128,9 @@ export type UserAdminUpdateDto = { shouldChangePassword?: boolean; storageLabel?: string | null; }; +export type CastResponse = { + gCastEnabled: boolean; +}; export type DownloadResponse = { archiveSize: number; includeEmbeddedVideos: boolean; @@ -164,6 +167,7 @@ export type TagsResponse = { sidebarWeb: boolean; }; export type UserPreferencesResponseDto = { + cast: CastResponse; download: DownloadResponse; emailNotifications: EmailNotificationsResponse; folders: FoldersResponse; @@ -177,6 +181,9 @@ export type UserPreferencesResponseDto = { export type AvatarUpdate = { color?: UserAvatarColor; }; +export type CastUpdate = { + gCastEnabled?: boolean; +}; export type DownloadUpdate = { archiveSize?: number; includeEmbeddedVideos?: boolean; @@ -214,6 +221,7 @@ export type TagsUpdate = { }; export type UserPreferencesUpdateDto = { avatar?: AvatarUpdate; + cast?: CastUpdate; download?: DownloadUpdate; emailNotifications?: EmailNotificationsUpdate; folders?: FoldersUpdate; diff --git a/server/src/dtos/user-preferences.dto.ts b/server/src/dtos/user-preferences.dto.ts index a9d32523aeb..43e15689b93 100644 --- a/server/src/dtos/user-preferences.dto.ts +++ b/server/src/dtos/user-preferences.dto.ts @@ -85,6 +85,11 @@ class PurchaseUpdate { hideBuyButtonUntil?: string; } +class CastUpdate { + @ValidateBoolean({ optional: true }) + gCastEnabled?: boolean; +} + export class UserPreferencesUpdateDto { @Optional() @ValidateNested() @@ -135,6 +140,11 @@ export class UserPreferencesUpdateDto { @ValidateNested() @Type(() => PurchaseUpdate) purchase?: PurchaseUpdate; + + @Optional() + @ValidateNested() + @Type(() => CastUpdate) + cast?: CastUpdate; } class RatingsResponse { @@ -183,6 +193,10 @@ class PurchaseResponse { hideBuyButtonUntil!: string; } +class CastResponse { + gCastEnabled: boolean = false; +} + export class UserPreferencesResponseDto implements UserPreferences { folders!: FoldersResponse; memories!: MemoriesResponse; @@ -193,6 +207,7 @@ export class UserPreferencesResponseDto implements UserPreferences { emailNotifications!: EmailNotificationsResponse; download!: DownloadResponse; purchase!: PurchaseResponse; + cast!: CastResponse; } export const mapPreferences = (preferences: UserPreferences): UserPreferencesResponseDto => { diff --git a/server/src/types.ts b/server/src/types.ts index d166a94e8b6..9d5ba46e120 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -502,6 +502,9 @@ export interface UserPreferences { showSupportBadge: boolean; hideBuyButtonUntil: string; }; + cast: { + gCastEnabled: boolean; + }; } export interface UserMetadata extends Record> { diff --git a/server/src/utils/preferences.ts b/server/src/utils/preferences.ts index a013c0b74ed..009dabce58c 100644 --- a/server/src/utils/preferences.ts +++ b/server/src/utils/preferences.ts @@ -42,6 +42,9 @@ const getDefaultPreferences = (): UserPreferences => { showSupportBadge: true, hideBuyButtonUntil: new Date(2022, 1, 12).toISOString(), }, + cast: { + gCastEnabled: false, + }, }; }; diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts index f77fbc7f200..f6a46143bc6 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts @@ -1,5 +1,6 @@ -import { resetSavedUser, user as userStore } from '$lib/stores/user.store'; +import { preferences as preferencesStore, resetSavedUser, user as userStore } from '$lib/stores/user.store'; import { assetFactory } from '@test-data/factories/asset-factory'; +import { preferencesFactory } from '@test-data/factories/preferences-factory'; import { userAdminFactory } from '@test-data/factories/user-factory'; import '@testing-library/jest-dom'; import { render } from '@testing-library/svelte'; @@ -42,6 +43,9 @@ describe('AssetViewerNavBar component', () => { }); it('shows back button', () => { + const prefs = preferencesFactory.build({ cast: { gCastEnabled: false } }); + preferencesStore.set(prefs); + const asset = assetFactory.build({ isTrashed: false }); const { getByTitle } = render(AssetViewerNavBar, { asset, ...additionalProps }); expect(getByTitle('go_back')).toBeInTheDocument(); @@ -53,6 +57,10 @@ describe('AssetViewerNavBar component', () => { const user = userAdminFactory.build({ id: ownerId }); const asset = assetFactory.build({ ownerId, isTrashed: false }); userStore.set(user); + + const prefs = preferencesFactory.build({ cast: { gCastEnabled: false } }); + preferencesStore.set(prefs); + const { getByTitle } = render(AssetViewerNavBar, { asset, ...additionalProps }); expect(getByTitle('delete')).toBeInTheDocument(); }); diff --git a/web/src/lib/components/user-settings-page/feature-settings.svelte b/web/src/lib/components/user-settings-page/feature-settings.svelte index d331e404320..b7db2c92a29 100644 --- a/web/src/lib/components/user-settings-page/feature-settings.svelte +++ b/web/src/lib/components/user-settings-page/feature-settings.svelte @@ -34,6 +34,9 @@ let tagsEnabled = $state($preferences?.tags?.enabled ?? false); let tagsSidebar = $state($preferences?.tags?.sidebarWeb ?? false); + // Cast + let gCastEnabled = $state($preferences?.cast?.gCastEnabled ?? false); + const handleSave = async () => { try { const data = await updateMyPreferences({ @@ -44,6 +47,7 @@ ratings: { enabled: ratingsEnabled }, sharedLinks: { enabled: sharedLinksEnabled, sidebarWeb: sharedLinkSidebar }, tags: { enabled: tagsEnabled, sidebarWeb: tagsSidebar }, + cast: { gCastEnabled }, }, }); @@ -138,6 +142,16 @@ {/if} + +
+ +
+
+
diff --git a/web/src/lib/utils/cast/gcast-destination.svelte.ts b/web/src/lib/utils/cast/gcast-destination.svelte.ts index fcfb8c382a6..f101c504f04 100644 --- a/web/src/lib/utils/cast/gcast-destination.svelte.ts +++ b/web/src/lib/utils/cast/gcast-destination.svelte.ts @@ -1,6 +1,8 @@ import { CastDestinationType, CastState, type ICastDestination } from '$lib/managers/cast-manager.svelte'; +import { preferences } from '$lib/stores/user.store'; import 'chromecast-caf-sender'; import { Duration } from 'luxon'; +import { get } from 'svelte/store'; const FRAMEWORK_LINK = 'https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1'; @@ -24,6 +26,12 @@ export class GCastDestination implements ICastDestination { private currentUrl: string | null = null; async initialize(): Promise { + const preferencesStore = get(preferences); + if (!preferencesStore.cast.gCastEnabled) { + this.isAvailable = false; + return false; + } + // this is a really messy way since google does a pseudo-callbak // in the form of a global window event. We will give Chrome 3 seconds to respond // or we will mark the destination as unavailable diff --git a/web/src/test-data/factories/preferences-factory.ts b/web/src/test-data/factories/preferences-factory.ts new file mode 100644 index 00000000000..d531bc1a99f --- /dev/null +++ b/web/src/test-data/factories/preferences-factory.ts @@ -0,0 +1,43 @@ +import type { UserPreferencesResponseDto } from '@immich/sdk'; +import { Sync } from 'factory.ts'; + +export const preferencesFactory = Sync.makeFactory({ + cast: { + gCastEnabled: false, + }, + download: { + archiveSize: 0, + includeEmbeddedVideos: false, + }, + emailNotifications: { + albumInvite: false, + albumUpdate: false, + enabled: false, + }, + folders: { + enabled: false, + sidebarWeb: false, + }, + memories: { + enabled: false, + }, + people: { + enabled: false, + sidebarWeb: false, + }, + purchase: { + hideBuyButtonUntil: '', + showSupportBadge: false, + }, + ratings: { + enabled: false, + }, + sharedLinks: { + enabled: false, + sidebarWeb: false, + }, + tags: { + enabled: false, + sidebarWeb: false, + }, +}); From be247395dbc592fd9d51916d61c0401e1eee1707 Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Wed, 28 May 2025 17:23:49 -0400 Subject: [PATCH 02/50] fix(server): deadlock when fetching vector count (#18728) move row count query --- server/src/repositories/database.repository.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/repositories/database.repository.ts b/server/src/repositories/database.repository.ts index 1e8e147c430..94d9029f60c 100644 --- a/server/src/repositories/database.repository.ts +++ b/server/src/repositories/database.repository.ts @@ -246,6 +246,7 @@ export class DatabaseRepository { return; } const dimSize = await this.getDimensionSize(table); + lists ||= this.targetListCount(await this.getRowCount(table)); await this.db.schema.dropIndex(indexName).ifExists().execute(); if (table === 'smart_search') { await this.db.schema.alterTable(table).dropConstraint('dim_size_constraint').ifExists().execute(); @@ -262,7 +263,6 @@ export class DatabaseRepository { ALTER TABLE ${sql.raw(table)} ALTER COLUMN embedding SET DATA TYPE ${sql.raw(schema)}vector(${sql.raw(String(dimSize))})`.execute(tx); - lists ||= this.targetListCount(await this.getRowCount(table)); await sql.raw(vectorIndexQuery({ vectorExtension, table, indexName, lists })).execute(tx); }); try { From 8ea40973a736db6b8b90a6c289f2b6b20fa9fef0 Mon Sep 17 00:00:00 2001 From: Nicholas <30300649+NicholasFlamy@users.noreply.github.com> Date: Wed, 28 May 2025 17:45:49 -0400 Subject: [PATCH 03/50] feat(server): apk links API endpoint for Obtainium Android mobile-server version sync (#18700) --- mobile/openapi/README.md | 2 + mobile/openapi/lib/api.dart | 1 + mobile/openapi/lib/api/server_api.dart | 41 ++++++ mobile/openapi/lib/api_client.dart | 2 + .../lib/model/server_apk_links_dto.dart | 123 ++++++++++++++++++ open-api/immich-openapi-specs.json | 55 ++++++++ open-api/typescript-sdk/src/fetch-client.ts | 14 ++ server/src/controllers/server.controller.ts | 7 + server/src/dtos/server.dto.ts | 7 + server/src/services/server.service.ts | 11 ++ 10 files changed, 263 insertions(+) create mode 100644 mobile/openapi/lib/model/server_apk_links_dto.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index b8bcbcebcd7..22264857e06 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -184,6 +184,7 @@ Class | Method | HTTP request | Description *SearchApi* | [**searchSmart**](doc//SearchApi.md#searchsmart) | **POST** /search/smart | *ServerApi* | [**deleteServerLicense**](doc//ServerApi.md#deleteserverlicense) | **DELETE** /server/license | *ServerApi* | [**getAboutInfo**](doc//ServerApi.md#getaboutinfo) | **GET** /server/about | +*ServerApi* | [**getAndroidLinks**](doc//ServerApi.md#getandroidlinks) | **GET** /server/android-links | *ServerApi* | [**getServerConfig**](doc//ServerApi.md#getserverconfig) | **GET** /server/config | *ServerApi* | [**getServerFeatures**](doc//ServerApi.md#getserverfeatures) | **GET** /server/features | *ServerApi* | [**getServerLicense**](doc//ServerApi.md#getserverlicense) | **GET** /server/license | @@ -419,6 +420,7 @@ Class | Method | HTTP request | Description - [SearchResponseDto](doc//SearchResponseDto.md) - [SearchSuggestionType](doc//SearchSuggestionType.md) - [ServerAboutResponseDto](doc//ServerAboutResponseDto.md) + - [ServerApkLinksDto](doc//ServerApkLinksDto.md) - [ServerConfigDto](doc//ServerConfigDto.md) - [ServerFeaturesDto](doc//ServerFeaturesDto.md) - [ServerMediaTypesResponseDto](doc//ServerMediaTypesResponseDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index ca33a5dea1b..846db953dcb 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -212,6 +212,7 @@ part 'model/search_facet_response_dto.dart'; part 'model/search_response_dto.dart'; part 'model/search_suggestion_type.dart'; part 'model/server_about_response_dto.dart'; +part 'model/server_apk_links_dto.dart'; part 'model/server_config_dto.dart'; part 'model/server_features_dto.dart'; part 'model/server_media_types_response_dto.dart'; diff --git a/mobile/openapi/lib/api/server_api.dart b/mobile/openapi/lib/api/server_api.dart index a0fd54f3d2b..4220e674714 100644 --- a/mobile/openapi/lib/api/server_api.dart +++ b/mobile/openapi/lib/api/server_api.dart @@ -90,6 +90,47 @@ class ServerApi { return null; } + /// Performs an HTTP 'GET /server/android-links' operation and returns the [Response]. + Future getAndroidLinksWithHttpInfo() async { + // ignore: prefer_const_declarations + final apiPath = r'/server/android-links'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future getAndroidLinks() async { + final response = await getAndroidLinksWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'ServerApkLinksDto',) as ServerApkLinksDto; + + } + return null; + } + /// Performs an HTTP 'GET /server/config' operation and returns the [Response]. Future getServerConfigWithHttpInfo() async { // ignore: prefer_const_declarations diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 1059655323a..2657cece1cc 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -480,6 +480,8 @@ class ApiClient { return SearchSuggestionTypeTypeTransformer().decode(value); case 'ServerAboutResponseDto': return ServerAboutResponseDto.fromJson(value); + case 'ServerApkLinksDto': + return ServerApkLinksDto.fromJson(value); case 'ServerConfigDto': return ServerConfigDto.fromJson(value); case 'ServerFeaturesDto': diff --git a/mobile/openapi/lib/model/server_apk_links_dto.dart b/mobile/openapi/lib/model/server_apk_links_dto.dart new file mode 100644 index 00000000000..086a2f172b3 --- /dev/null +++ b/mobile/openapi/lib/model/server_apk_links_dto.dart @@ -0,0 +1,123 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class ServerApkLinksDto { + /// Returns a new [ServerApkLinksDto] instance. + ServerApkLinksDto({ + required this.arm64v8a, + required this.armeabiv7a, + required this.universal, + required this.x8664, + }); + + String arm64v8a; + + String armeabiv7a; + + String universal; + + String x8664; + + @override + bool operator ==(Object other) => identical(this, other) || other is ServerApkLinksDto && + other.arm64v8a == arm64v8a && + other.armeabiv7a == armeabiv7a && + other.universal == universal && + other.x8664 == x8664; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (arm64v8a.hashCode) + + (armeabiv7a.hashCode) + + (universal.hashCode) + + (x8664.hashCode); + + @override + String toString() => 'ServerApkLinksDto[arm64v8a=$arm64v8a, armeabiv7a=$armeabiv7a, universal=$universal, x8664=$x8664]'; + + Map toJson() { + final json = {}; + json[r'arm64v8a'] = this.arm64v8a; + json[r'armeabiv7a'] = this.armeabiv7a; + json[r'universal'] = this.universal; + json[r'x86_64'] = this.x8664; + return json; + } + + /// Returns a new [ServerApkLinksDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static ServerApkLinksDto? fromJson(dynamic value) { + upgradeDto(value, "ServerApkLinksDto"); + if (value is Map) { + final json = value.cast(); + + return ServerApkLinksDto( + arm64v8a: mapValueOfType(json, r'arm64v8a')!, + armeabiv7a: mapValueOfType(json, r'armeabiv7a')!, + universal: mapValueOfType(json, r'universal')!, + x8664: mapValueOfType(json, r'x86_64')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = ServerApkLinksDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = ServerApkLinksDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of ServerApkLinksDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = ServerApkLinksDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'arm64v8a', + 'armeabiv7a', + 'universal', + 'x86_64', + }; +} + diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 6aca3c349ad..dee027f8386 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -5275,6 +5275,38 @@ ] } }, + "/server/android-links": { + "get": { + "operationId": "getAndroidLinks", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServerApkLinksDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Server" + ] + } + }, "/server/config": { "get": { "operationId": "getServerConfig", @@ -11959,6 +11991,29 @@ ], "type": "object" }, + "ServerApkLinksDto": { + "properties": { + "arm64v8a": { + "type": "string" + }, + "armeabiv7a": { + "type": "string" + }, + "universal": { + "type": "string" + }, + "x86_64": { + "type": "string" + } + }, + "required": [ + "arm64v8a", + "armeabiv7a", + "universal", + "x86_64" + ], + "type": "object" + }, "ServerConfigDto": { "properties": { "externalDomain": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 050fabca927..0c0ffd97963 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1004,6 +1004,12 @@ export type ServerAboutResponseDto = { version: string; versionUrl: string; }; +export type ServerApkLinksDto = { + arm64v8a: string; + armeabiv7a: string; + universal: string; + x86_64: string; +}; export type ServerConfigDto = { externalDomain: string; isInitialized: boolean; @@ -2868,6 +2874,14 @@ export function getAboutInfo(opts?: Oazapfts.RequestOpts) { ...opts })); } +export function getAndroidLinks(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: ServerApkLinksDto; + }>("/server/android-links", { + ...opts + })); +} export function getServerConfig(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; diff --git a/server/src/controllers/server.controller.ts b/server/src/controllers/server.controller.ts index 267fc42ef4a..5bc78574c6b 100644 --- a/server/src/controllers/server.controller.ts +++ b/server/src/controllers/server.controller.ts @@ -3,6 +3,7 @@ import { ApiNotFoundResponse, ApiTags } from '@nestjs/swagger'; import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; import { ServerAboutResponseDto, + ServerApkLinksDto, ServerConfigDto, ServerFeaturesDto, ServerMediaTypesResponseDto, @@ -34,6 +35,12 @@ export class ServerController { return this.service.getAboutInfo(); } + @Get('android-links') + @Authenticated() + getAndroidLinks(): ServerApkLinksDto { + return this.service.getAndroidLinks(); + } + @Get('storage') @Authenticated() getStorage(): Promise { diff --git a/server/src/dtos/server.dto.ts b/server/src/dtos/server.dto.ts index e1f94dbaa55..47442ad4fbc 100644 --- a/server/src/dtos/server.dto.ts +++ b/server/src/dtos/server.dto.ts @@ -37,6 +37,13 @@ export class ServerAboutResponseDto { thirdPartySupportUrl?: string; } +export class ServerApkLinksDto { + arm64v8a!: string; + armeabiv7a!: string; + universal!: string; + x86_64!: string; +} + export class ServerStorageResponseDto { diskSize!: string; diskUse!: string; diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts index 9112c40a17d..f07b8ee92e0 100644 --- a/server/src/services/server.service.ts +++ b/server/src/services/server.service.ts @@ -5,6 +5,7 @@ import { OnEvent } from 'src/decorators'; import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; import { ServerAboutResponseDto, + ServerApkLinksDto, ServerConfigDto, ServerFeaturesDto, ServerMediaTypesResponseDto, @@ -48,6 +49,16 @@ export class ServerService extends BaseService { }; } + getAndroidLinks(): ServerApkLinksDto { + const baseURL = `https://github.com/immich-app/immich/releases/download/v${serverVersion.toString()}`; + return { + arm64v8a: `${baseURL}/app-arm64-v8a-release.apk`, + armeabiv7a: `${baseURL}/app-armeabi-v7a-release.apk`, + universal: `${baseURL}/app-release.apk`, + x86_64: `${baseURL}/app-x86_64-release.apk`, + }; + } + async getStorage(): Promise { const libraryBase = StorageCore.getBaseFolder(StorageFolder.LIBRARY); const diskInfo = await this.storageRepository.checkDiskUsage(libraryBase); From 10181defb1b082d249f8d5a698bc7c9093cec2b5 Mon Sep 17 00:00:00 2001 From: Arno <46051866+arnolicious@users.noreply.github.com> Date: Thu, 29 May 2025 12:30:25 +0200 Subject: [PATCH 04/50] chore: Refactor Edit Album Modal (#18653) --- .../components/album-page/albums-list.svelte | 24 ++++++------------- .../components/forms/edit-album-form.svelte | 10 ++++---- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/web/src/lib/components/album-page/albums-list.svelte b/web/src/lib/components/album-page/albums-list.svelte index da036dd0392..32cccbc850b 100644 --- a/web/src/lib/components/album-page/albums-list.svelte +++ b/web/src/lib/components/album-page/albums-list.svelte @@ -143,7 +143,6 @@ let albumGroupOption: string = $state(AlbumGroupBy.None); - let albumToEdit: AlbumResponseDto | null = $state(null); let albumToShare: AlbumResponseDto | null = $state(null); let albumToDelete: AlbumResponseDto | null = null; @@ -257,9 +256,14 @@ await deleteSelectedAlbum(); }; - const handleEdit = (album: AlbumResponseDto) => { - albumToEdit = album; + const handleEdit = async (album: AlbumResponseDto) => { closeAlbumContextMenu(); + const editedAlbum = await modalManager.show(EditAlbumForm, { + album, + }); + if (editedAlbum) { + successEditAlbumInfo(editedAlbum); + } }; const deleteSelectedAlbum = async () => { @@ -305,8 +309,6 @@ }; const successEditAlbumInfo = (album: AlbumResponseDto) => { - albumToEdit = null; - notificationController.show({ message: $t('album_info_updated'), type: NotificationType.Info, @@ -422,15 +424,3 @@ setAlbumToDelete()} /> {/if} - -{#if allowEdit} - - {#if albumToEdit} - (albumToEdit = null)} - onClose={() => (albumToEdit = null)} - /> - {/if} -{/if} diff --git a/web/src/lib/components/forms/edit-album-form.svelte b/web/src/lib/components/forms/edit-album-form.svelte index 32e1e422e7c..c6ad3d600a6 100644 --- a/web/src/lib/components/forms/edit-album-form.svelte +++ b/web/src/lib/components/forms/edit-album-form.svelte @@ -8,12 +8,10 @@ interface Props { album: AlbumResponseDto; - onEditSuccess?: ((album: AlbumResponseDto) => unknown) | undefined; - onCancel?: (() => unknown) | undefined; - onClose: () => void; + onClose: (album?: AlbumResponseDto) => void; } - let { album = $bindable(), onEditSuccess = undefined, onCancel = undefined, onClose }: Props = $props(); + let { album = $bindable(), onClose }: Props = $props(); let albumName = $state(album.albumName); let description = $state(album.description); @@ -32,7 +30,7 @@ }); album.albumName = albumName; album.description = description; - onEditSuccess?.(album); + onClose(album); } catch (error) { handleError(error, $t('errors.unable_to_update_album_info')); } finally { @@ -71,7 +69,7 @@
- +
From 6f39a706b2e409c8e6a1fa7654e6fae52d919d44 Mon Sep 17 00:00:00 2001 From: Daimolean <92239625+wuzihao051119@users.noreply.github.com> Date: Thu, 29 May 2025 21:48:44 +0800 Subject: [PATCH 05/50] fix: missing permissions and optional update (#18735) * fix: missing permissions * fix: test --- .../openapi/lib/model/api_key_update_dto.dart | 20 +++++++++++++------ open-api/immich-openapi-specs.json | 4 ---- open-api/typescript-sdk/src/fetch-client.ts | 4 ++-- server/src/dtos/api-key.dto.ts | 6 ++++-- web/src/lib/modals/ApiKeyModal.svelte | 8 +++++++- 5 files changed, 27 insertions(+), 15 deletions(-) diff --git a/mobile/openapi/lib/model/api_key_update_dto.dart b/mobile/openapi/lib/model/api_key_update_dto.dart index 60ac168fdbd..7f32c951187 100644 --- a/mobile/openapi/lib/model/api_key_update_dto.dart +++ b/mobile/openapi/lib/model/api_key_update_dto.dart @@ -13,11 +13,17 @@ part of openapi.api; class APIKeyUpdateDto { /// Returns a new [APIKeyUpdateDto] instance. APIKeyUpdateDto({ - required this.name, + this.name, this.permissions = const [], }); - String name; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? name; List permissions; @@ -29,7 +35,7 @@ class APIKeyUpdateDto { @override int get hashCode => // ignore: unnecessary_parenthesis - (name.hashCode) + + (name == null ? 0 : name!.hashCode) + (permissions.hashCode); @override @@ -37,7 +43,11 @@ class APIKeyUpdateDto { Map toJson() { final json = {}; + if (this.name != null) { json[r'name'] = this.name; + } else { + // json[r'name'] = null; + } json[r'permissions'] = this.permissions; return json; } @@ -51,7 +61,7 @@ class APIKeyUpdateDto { final json = value.cast(); return APIKeyUpdateDto( - name: mapValueOfType(json, r'name')!, + name: mapValueOfType(json, r'name'), permissions: Permission.listFromJson(json[r'permissions']), ); } @@ -100,8 +110,6 @@ class APIKeyUpdateDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'name', - 'permissions', }; } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index dee027f8386..410840388f2 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -8335,10 +8335,6 @@ "type": "array" } }, - "required": [ - "name", - "permissions" - ], "type": "object" }, "ActivityCreateDto": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 0c0ffd97963..2b0e2849d12 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -415,8 +415,8 @@ export type ApiKeyCreateResponseDto = { secret: string; }; export type ApiKeyUpdateDto = { - name: string; - permissions: Permission[]; + name?: string; + permissions?: Permission[]; }; export type AssetBulkDeleteDto = { force?: boolean; diff --git a/server/src/dtos/api-key.dto.ts b/server/src/dtos/api-key.dto.ts index ac6dd25bcf5..c790ea613dd 100644 --- a/server/src/dtos/api-key.dto.ts +++ b/server/src/dtos/api-key.dto.ts @@ -15,14 +15,16 @@ export class APIKeyCreateDto { } export class APIKeyUpdateDto { + @Optional() @IsString() @IsNotEmpty() - name!: string; + name?: string; + @Optional() @IsEnum(Permission, { each: true }) @ApiProperty({ enum: Permission, enumName: 'Permission', isArray: true }) @ArrayMinSize(1) - permissions!: Permission[]; + permissions?: Permission[]; } export class APIKeyCreateResponseDto { diff --git a/web/src/lib/modals/ApiKeyModal.svelte b/web/src/lib/modals/ApiKeyModal.svelte index 4a1df0f9813..a0f8d57193b 100644 --- a/web/src/lib/modals/ApiKeyModal.svelte +++ b/web/src/lib/modals/ApiKeyModal.svelte @@ -110,7 +110,13 @@ Permission.PersonReassign, ]); - permissions.set('session', [Permission.SessionRead, Permission.SessionUpdate, Permission.SessionDelete]); + permissions.set('session', [ + Permission.SessionCreate, + Permission.SessionRead, + Permission.SessionUpdate, + Permission.SessionDelete, + Permission.SessionLock, + ]); permissions.set('sharedLink', [ Permission.SharedLinkCreate, From 0257f1a74316e04b92283f049b0139b224a3b435 Mon Sep 17 00:00:00 2001 From: Brandon Wees Date: Thu, 29 May 2025 09:06:13 -0500 Subject: [PATCH 06/50] chore(mobile): add default cast user pref to openapi patching (#18747) add default cast user pref to mobile patching --- mobile/lib/utils/openapi_patching.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/mobile/lib/utils/openapi_patching.dart b/mobile/lib/utils/openapi_patching.dart index 7c7d9bab882..58c3ef83947 100644 --- a/mobile/lib/utils/openapi_patching.dart +++ b/mobile/lib/utils/openapi_patching.dart @@ -11,6 +11,7 @@ dynamic upgradeDto(dynamic value, String targetType) { addDefault(value, 'people', PeopleResponse().toJson()); addDefault(value, 'tags', TagsResponse().toJson()); addDefault(value, 'sharedLinks', SharedLinksResponse().toJson()); + addDefault(value, 'cast', CastResponse().toJson()); } break; case 'ServerConfigDto': From 1f18a090618368545a714f300cabe9356bc8256f Mon Sep 17 00:00:00 2001 From: Daimolean <92239625+wuzihao051119@users.noreply.github.com> Date: Thu, 29 May 2025 22:13:44 +0800 Subject: [PATCH 07/50] fix(web): hide map button when disable (#18743) --- web/src/lib/components/album-page/album-viewer.svelte | 5 +++-- .../[[photos=photos]]/[[assetId=id]]/+page.svelte | 5 ++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/web/src/lib/components/album-page/album-viewer.svelte b/web/src/lib/components/album-page/album-viewer.svelte index f8a38d9e673..0aa4e0ed3c2 100644 --- a/web/src/lib/components/album-page/album-viewer.svelte +++ b/web/src/lib/components/album-page/album-viewer.svelte @@ -1,5 +1,6 @@ -{#if addExclusionPattern} - (addExclusionPattern = false)} - /> -{/if} - -{#if editExclusionPattern != undefined} - (editExclusionPattern = null)} - /> -{/if} -
@@ -136,10 +122,7 @@ icon={mdiPencilOutline} title={$t('edit_exclusion_pattern')} size="16" - onclick={() => { - editExclusionPattern = listIndex; - editedExclusionPattern = exclusionPattern; - }} + onclick={() => onEditExclusionPattern(listIndex)} /> @@ -153,13 +136,9 @@ {/if} diff --git a/web/src/lib/components/forms/library-exclusion-pattern-form.svelte b/web/src/lib/modals/LibraryExclusionPatternModal.svelte similarity index 84% rename from web/src/lib/components/forms/library-exclusion-pattern-form.svelte rename to web/src/lib/modals/LibraryExclusionPatternModal.svelte index e069e5c7a27..d182a89684b 100644 --- a/web/src/lib/components/forms/library-exclusion-pattern-form.svelte +++ b/web/src/lib/modals/LibraryExclusionPatternModal.svelte @@ -9,9 +9,7 @@ exclusionPatterns?: string[]; isEditing?: boolean; submitText?: string; - onCancel: () => void; - onSubmit: (exclusionPattern: string) => void; - onDelete?: () => void; + onClose: (data?: { action: 'delete' } | { action: 'submit'; exclusionPattern: string }) => void; } let { @@ -19,9 +17,7 @@ exclusionPatterns = $bindable([]), isEditing = false, submitText = $t('submit'), - onCancel, - onSubmit, - onDelete, + onClose, }: Props = $props(); onMount(() => { @@ -36,12 +32,12 @@ const onsubmit = (event: Event) => { event.preventDefault(); if (canSubmit) { - onSubmit(exclusionPattern); + onClose({ action: 'submit', exclusionPattern }); } }; - +

@@ -68,13 +64,15 @@

- + {#if isEditing} - + {/if} - +
From dbdb64f6c55c9b75b2a3168995563c7c2c413f41 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Thu, 29 May 2025 21:12:00 +0530 Subject: [PATCH 11/50] feat: delta sync (#18428) * feat: delta sync * fix: ignore iCloud assets * feat: dev logs * add full sync button * remove photo_manager dep for sync * misc logs and fix * add time taken to DLog * fix: build release iOS * ios sync go brrr * rename local sync service * update isar fork * rename to platform assets / albums * fix ci check --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex --- .gitattributes | 3 + .github/workflows/build-mobile.yml | 4 + .github/workflows/static_analysis.yml | 6 +- mobile/analysis_options.yaml | 1 + mobile/android/app/build.gradle | 137 ++-- .../app/alextran/immich/MainActivity.kt | 13 + .../app/alextran/immich/sync/Messages.g.kt | 393 +++++++++++ .../alextran/immich/sync/MessagesImpl26.kt | 24 + .../alextran/immich/sync/MessagesImpl30.kt | 89 +++ .../alextran/immich/sync/MessagesImplBase.kt | 177 +++++ mobile/android/settings.gradle | 35 +- mobile/immich_lint/pubspec.lock | 55 +- mobile/immich_lint/pubspec.yaml | 6 +- mobile/ios/Runner.xcodeproj/project.pbxproj | 16 +- mobile/ios/Runner/AppDelegate.swift | 3 + mobile/ios/Runner/Sync/Messages.g.swift | 446 ++++++++++++ mobile/ios/Runner/Sync/MessagesImpl.swift | 246 +++++++ mobile/lib/constants/constants.dart | 1 + .../interfaces/local_album.interface.dart | 34 + .../lib/domain/models/asset/asset.model.dart | 47 ++ .../domain/models/asset/base_asset.model.dart | 76 ++ .../models/asset/local_asset.model.dart | 74 ++ .../lib/domain/models/local_album.model.dart | 70 ++ .../domain/services/local_sync.service.dart | 379 ++++++++++ mobile/lib/domain/utils/background_sync.dart | 33 +- .../entities/local_album.entity.dart | 18 + .../entities/local_album.entity.drift.dart | 497 +++++++++++++ .../entities/local_album_asset.entity.dart | 17 + .../local_album_asset.entity.drift.dart | 565 +++++++++++++++ .../entities/local_asset.entity.dart | 17 + .../entities/local_asset.entity.drift.dart | 658 ++++++++++++++++++ .../repositories/db.repository.dart | 17 +- .../repositories/db.repository.drift.dart | 45 +- .../repositories/local_album.repository.dart | 366 ++++++++++ .../lib/infrastructure/utils/asset.mixin.dart | 10 + mobile/lib/platform/native_sync_api.g.dart | 501 +++++++++++++ .../presentation/pages/dev/dev_logger.dart | 68 ++ .../pages/dev/feat_in_development.page.dart | 174 +++++ .../pages/dev/local_media_stat.page.dart | 125 ++++ .../infrastructure/album.provider.dart | 8 + .../infrastructure/platform.provider.dart | 4 + ...tream.provider.dart => sync.provider.dart} | 12 + mobile/lib/routing/router.dart | 10 + mobile/lib/routing/router.gr.dart | 415 ++++------- mobile/lib/widgets/common/immich_app_bar.dart | 7 +- mobile/makefile | 11 +- mobile/pigeon/native_sync_api.dart | 89 +++ mobile/pubspec.lock | 98 +-- mobile/pubspec.yaml | 22 +- 49 files changed, 5634 insertions(+), 488 deletions(-) create mode 100644 mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt create mode 100644 mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImpl26.kt create mode 100644 mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImpl30.kt create mode 100644 mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt create mode 100644 mobile/ios/Runner/Sync/Messages.g.swift create mode 100644 mobile/ios/Runner/Sync/MessagesImpl.swift create mode 100644 mobile/lib/domain/interfaces/local_album.interface.dart create mode 100644 mobile/lib/domain/models/asset/asset.model.dart create mode 100644 mobile/lib/domain/models/asset/base_asset.model.dart create mode 100644 mobile/lib/domain/models/asset/local_asset.model.dart create mode 100644 mobile/lib/domain/models/local_album.model.dart create mode 100644 mobile/lib/domain/services/local_sync.service.dart create mode 100644 mobile/lib/infrastructure/entities/local_album.entity.dart create mode 100644 mobile/lib/infrastructure/entities/local_album.entity.drift.dart create mode 100644 mobile/lib/infrastructure/entities/local_album_asset.entity.dart create mode 100644 mobile/lib/infrastructure/entities/local_album_asset.entity.drift.dart create mode 100644 mobile/lib/infrastructure/entities/local_asset.entity.dart create mode 100644 mobile/lib/infrastructure/entities/local_asset.entity.drift.dart create mode 100644 mobile/lib/infrastructure/repositories/local_album.repository.dart create mode 100644 mobile/lib/infrastructure/utils/asset.mixin.dart create mode 100644 mobile/lib/platform/native_sync_api.g.dart create mode 100644 mobile/lib/presentation/pages/dev/dev_logger.dart create mode 100644 mobile/lib/presentation/pages/dev/feat_in_development.page.dart create mode 100644 mobile/lib/presentation/pages/dev/local_media_stat.page.dart create mode 100644 mobile/lib/providers/infrastructure/album.provider.dart create mode 100644 mobile/lib/providers/infrastructure/platform.provider.dart rename mobile/lib/providers/infrastructure/{sync_stream.provider.dart => sync.provider.dart} (64%) create mode 100644 mobile/pigeon/native_sync_api.dart diff --git a/.gitattributes b/.gitattributes index 2e8a45ca5c3..3d43ff20ed9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -9,6 +9,9 @@ mobile/lib/**/*.g.dart linguist-generated=true mobile/lib/**/*.drift.dart -diff -merge mobile/lib/**/*.drift.dart linguist-generated=true +mobile/drift_schemas/main/drift_schema_*.json -diff -merge +mobile/drift_schemas/main/drift_schema_*.json linguist-generated=true + open-api/typescript-sdk/fetch-client.ts -diff -merge open-api/typescript-sdk/fetch-client.ts linguist-generated=true diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index 0fcc4f1d7c9..33912d687cb 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -93,6 +93,10 @@ jobs: run: make translation working-directory: ./mobile + - name: Generate platform APIs + run: make pigeon + working-directory: ./mobile + - name: Build Android App Bundle working-directory: ./mobile env: diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 7cd28228dc2..754c0c38b31 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -59,13 +59,17 @@ jobs: working-directory: ./mobile - name: Generate translation file - run: make translation; dart format lib/generated/codegen_loader.g.dart + run: make translation working-directory: ./mobile - name: Run Build Runner run: make build working-directory: ./mobile + - name: Generate platform API + run: make pigeon + working-directory: ./mobile + - name: Find file changes uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 id: verify-changed-files diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index 854f852e3cc..dc81c10dece 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -55,6 +55,7 @@ custom_lint: restrict: package:photo_manager allowed: # required / wanted + - 'lib/infrastructure/repositories/album_media.repository.dart' - 'lib/repositories/{album,asset,file}_media.repository.dart' # acceptable exceptions for the time being - lib/entities/asset.entity.dart # to provide local AssetEntity for now diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index 0ec511d9f12..7455ae99a25 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -1,103 +1,106 @@ plugins { - id "com.android.application" - id "kotlin-android" - id "dev.flutter.flutter-gradle-plugin" - id 'com.google.devtools.ksp' + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" + id 'com.google.devtools.ksp' } def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { - localPropertiesFile.withInputStream { localProperties.load(it) } + localPropertiesFile.withInputStream { localProperties.load(it) } } def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { - flutterVersionCode = '1' + flutterVersionCode = '1' } def flutterVersionName = localProperties.getProperty('flutter.versionName') if (flutterVersionName == null) { - flutterVersionName = '1.0' + flutterVersionName = '1.0' } def keystoreProperties = new Properties() def keystorePropertiesFile = rootProject.file('key.properties') if (keystorePropertiesFile.exists()) { - keystorePropertiesFile.withInputStream { keystoreProperties.load(it) } + keystorePropertiesFile.withInputStream { keystoreProperties.load(it) } } android { - compileSdkVersion 35 + compileSdkVersion 35 - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - coreLibraryDesugaringEnabled true + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + coreLibraryDesugaringEnabled true + } + + kotlinOptions { + jvmTarget = '17' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + applicationId "app.alextran.immich" + minSdkVersion 26 + targetSdkVersion 35 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + signingConfigs { + release { + def keyAliasVal = System.getenv("ALIAS") + def keyPasswordVal = System.getenv("ANDROID_KEY_PASSWORD") + def storePasswordVal = System.getenv("ANDROID_STORE_PASSWORD") + + keyAlias keyAliasVal ? keyAliasVal : keystoreProperties['keyAlias'] + keyPassword keyPasswordVal ? keyPasswordVal : keystoreProperties['keyPassword'] + storeFile file("../key.jks") ? file("../key.jks") : file(keystoreProperties['storeFile']) + storePassword storePasswordVal ? storePasswordVal : keystoreProperties['storePassword'] + } + } + + buildTypes { + debug { + applicationIdSuffix '.debug' + versionNameSuffix '-DEBUG' } - kotlinOptions { - jvmTarget = '17' + release { + signingConfig signingConfigs.release } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - - defaultConfig { - applicationId "app.alextran.immich" - minSdkVersion 26 - targetSdkVersion 35 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - } - - signingConfigs { - release { - def keyAliasVal = System.getenv("ALIAS") - def keyPasswordVal = System.getenv("ANDROID_KEY_PASSWORD") - def storePasswordVal = System.getenv("ANDROID_STORE_PASSWORD") - - keyAlias keyAliasVal ? keyAliasVal : keystoreProperties['keyAlias'] - keyPassword keyPasswordVal ? keyPasswordVal : keystoreProperties['keyPassword'] - storeFile file("../key.jks") ? file("../key.jks") : file(keystoreProperties['storeFile']) - storePassword storePasswordVal ? storePasswordVal : keystoreProperties['storePassword'] - } - } - - buildTypes { - debug { - applicationIdSuffix '.debug' - versionNameSuffix '-DEBUG' - } - - release { - signingConfig signingConfigs.release - } - } - namespace 'app.alextran.immich' + } + namespace 'app.alextran.immich' } flutter { - source '../..' + source '../..' } dependencies { - def kotlin_version = '2.0.20' - def kotlin_coroutines_version = '1.9.0' - def work_version = '2.9.1' - def concurrent_version = '1.2.0' - def guava_version = '33.3.1-android' - def glide_version = '4.16.0' + def kotlin_version = '2.0.20' + def kotlin_coroutines_version = '1.9.0' + def work_version = '2.9.1' + def concurrent_version = '1.2.0' + def guava_version = '33.3.1-android' + def glide_version = '4.16.0' + def serialization_version = '1.8.1' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" - implementation "androidx.work:work-runtime-ktx:$work_version" - implementation "androidx.concurrent:concurrent-futures:$concurrent_version" - implementation "com.google.guava:guava:$guava_version" - implementation "com.github.bumptech.glide:glide:$glide_version" - ksp "com.github.bumptech.glide:ksp:$glide_version" - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.2' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" + implementation "androidx.work:work-runtime-ktx:$work_version" + implementation "androidx.concurrent:concurrent-futures:$concurrent_version" + implementation "com.google.guava:guava:$guava_version" + implementation "com.github.bumptech.glide:glide:$glide_version" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization_version" + + ksp "com.github.bumptech.glide:ksp:$glide_version" + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.2' } // This is uncommented in F-Droid build script diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt index c1e5152d28b..f9c4ee2a1ff 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt @@ -1,6 +1,11 @@ package app.alextran.immich +import android.os.Build +import android.os.ext.SdkExtensions import androidx.annotation.NonNull +import app.alextran.immich.sync.NativeSyncApi +import app.alextran.immich.sync.NativeSyncApiImpl26 +import app.alextran.immich.sync.NativeSyncApiImpl30 import io.flutter.embedding.android.FlutterFragmentActivity import io.flutter.embedding.engine.FlutterEngine @@ -10,5 +15,13 @@ class MainActivity : FlutterFragmentActivity() { flutterEngine.plugins.add(BackgroundServicePlugin()) flutterEngine.plugins.add(HttpSSLOptionsPlugin()) // No need to set up method channel here as it's now handled in the plugin + + val nativeSyncApiImpl = + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R || SdkExtensions.getExtensionVersion(Build.VERSION_CODES.R) < 1) { + NativeSyncApiImpl26(this) + } else { + NativeSyncApiImpl30(this) + } + NativeSyncApi.setUp(flutterEngine.dartExecutor.binaryMessenger, nativeSyncApiImpl) } } diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt new file mode 100644 index 00000000000..f4dbda730b0 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt @@ -0,0 +1,393 @@ +// Autogenerated from Pigeon (v25.3.2), do not edit directly. +// See also: https://pub.dev/packages/pigeon +@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") + +package app.alextran.immich.sync + +import android.util.Log +import io.flutter.plugin.common.BasicMessageChannel +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MessageCodec +import io.flutter.plugin.common.StandardMethodCodec +import io.flutter.plugin.common.StandardMessageCodec +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer +private object MessagesPigeonUtils { + + fun wrapResult(result: Any?): List { + return listOf(result) + } + + fun wrapError(exception: Throwable): List { + return if (exception is FlutterError) { + listOf( + exception.code, + exception.message, + exception.details + ) + } else { + listOf( + exception.javaClass.simpleName, + exception.toString(), + "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) + ) + } + } + fun deepEquals(a: Any?, b: Any?): Boolean { + if (a is ByteArray && b is ByteArray) { + return a.contentEquals(b) + } + if (a is IntArray && b is IntArray) { + return a.contentEquals(b) + } + if (a is LongArray && b is LongArray) { + return a.contentEquals(b) + } + if (a is DoubleArray && b is DoubleArray) { + return a.contentEquals(b) + } + if (a is Array<*> && b is Array<*>) { + return a.size == b.size && + a.indices.all{ deepEquals(a[it], b[it]) } + } + if (a is List<*> && b is List<*>) { + return a.size == b.size && + a.indices.all{ deepEquals(a[it], b[it]) } + } + if (a is Map<*, *> && b is Map<*, *>) { + return a.size == b.size && a.all { + (b as Map).containsKey(it.key) && + deepEquals(it.value, b[it.key]) + } + } + return a == b + } + +} + +/** + * Error class for passing custom error details to Flutter via a thrown PlatformException. + * @property code The error code. + * @property message The error message. + * @property details The error details. Must be a datatype supported by the api codec. + */ +class FlutterError ( + val code: String, + override val message: String? = null, + val details: Any? = null +) : Throwable() + +/** Generated class from Pigeon that represents data sent in messages. */ +data class PlatformAsset ( + val id: String, + val name: String, + val type: Long, + val createdAt: Long? = null, + val updatedAt: Long? = null, + val durationInSeconds: Long +) + { + companion object { + fun fromList(pigeonVar_list: List): PlatformAsset { + val id = pigeonVar_list[0] as String + val name = pigeonVar_list[1] as String + val type = pigeonVar_list[2] as Long + val createdAt = pigeonVar_list[3] as Long? + val updatedAt = pigeonVar_list[4] as Long? + val durationInSeconds = pigeonVar_list[5] as Long + return PlatformAsset(id, name, type, createdAt, updatedAt, durationInSeconds) + } + } + fun toList(): List { + return listOf( + id, + name, + type, + createdAt, + updatedAt, + durationInSeconds, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is PlatformAsset) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class PlatformAlbum ( + val id: String, + val name: String, + val updatedAt: Long? = null, + val isCloud: Boolean, + val assetCount: Long +) + { + companion object { + fun fromList(pigeonVar_list: List): PlatformAlbum { + val id = pigeonVar_list[0] as String + val name = pigeonVar_list[1] as String + val updatedAt = pigeonVar_list[2] as Long? + val isCloud = pigeonVar_list[3] as Boolean + val assetCount = pigeonVar_list[4] as Long + return PlatformAlbum(id, name, updatedAt, isCloud, assetCount) + } + } + fun toList(): List { + return listOf( + id, + name, + updatedAt, + isCloud, + assetCount, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is PlatformAlbum) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class SyncDelta ( + val hasChanges: Boolean, + val updates: List, + val deletes: List, + val assetAlbums: Map> +) + { + companion object { + fun fromList(pigeonVar_list: List): SyncDelta { + val hasChanges = pigeonVar_list[0] as Boolean + val updates = pigeonVar_list[1] as List + val deletes = pigeonVar_list[2] as List + val assetAlbums = pigeonVar_list[3] as Map> + return SyncDelta(hasChanges, updates, deletes, assetAlbums) + } + } + fun toList(): List { + return listOf( + hasChanges, + updates, + deletes, + assetAlbums, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is SyncDelta) { + return false + } + if (this === other) { + return true + } + return MessagesPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} +private open class MessagesPigeonCodec : StandardMessageCodec() { + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return when (type) { + 129.toByte() -> { + return (readValue(buffer) as? List)?.let { + PlatformAsset.fromList(it) + } + } + 130.toByte() -> { + return (readValue(buffer) as? List)?.let { + PlatformAlbum.fromList(it) + } + } + 131.toByte() -> { + return (readValue(buffer) as? List)?.let { + SyncDelta.fromList(it) + } + } + else -> super.readValueOfType(type, buffer) + } + } + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + when (value) { + is PlatformAsset -> { + stream.write(129) + writeValue(stream, value.toList()) + } + is PlatformAlbum -> { + stream.write(130) + writeValue(stream, value.toList()) + } + is SyncDelta -> { + stream.write(131) + writeValue(stream, value.toList()) + } + else -> super.writeValue(stream, value) + } + } +} + +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface NativeSyncApi { + fun shouldFullSync(): Boolean + fun getMediaChanges(): SyncDelta + fun checkpointSync() + fun clearSyncCheckpoint() + fun getAssetIdsForAlbum(albumId: String): List + fun getAlbums(): List + fun getAssetsCountSince(albumId: String, timestamp: Long): Long + fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List + + companion object { + /** The codec used by NativeSyncApi. */ + val codec: MessageCodec by lazy { + MessagesPigeonCodec() + } + /** Sets up an instance of `NativeSyncApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: NativeSyncApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val taskQueue = binaryMessenger.makeBackgroundTaskQueue() + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + listOf(api.shouldFullSync()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges$separatedMessageChannelSuffix", codec, taskQueue) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + listOf(api.getMediaChanges()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.checkpointSync$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + api.checkpointSync() + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.clearSyncCheckpoint$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + api.clearSyncCheckpoint() + listOf(null) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum$separatedMessageChannelSuffix", codec, taskQueue) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val albumIdArg = args[0] as String + val wrapped: List = try { + listOf(api.getAssetIdsForAlbum(albumIdArg)) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums$separatedMessageChannelSuffix", codec, taskQueue) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + listOf(api.getAlbums()) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince$separatedMessageChannelSuffix", codec, taskQueue) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val albumIdArg = args[0] as String + val timestampArg = args[1] as Long + val wrapped: List = try { + listOf(api.getAssetsCountSince(albumIdArg, timestampArg)) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum$separatedMessageChannelSuffix", codec, taskQueue) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val albumIdArg = args[0] as String + val updatedTimeCondArg = args[1] as Long? + val wrapped: List = try { + listOf(api.getAssetsForAlbum(albumIdArg, updatedTimeCondArg)) + } catch (exception: Throwable) { + MessagesPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImpl26.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImpl26.kt new file mode 100644 index 00000000000..5deacc30db1 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImpl26.kt @@ -0,0 +1,24 @@ +package app.alextran.immich.sync + +import android.content.Context + + +class NativeSyncApiImpl26(context: Context) : NativeSyncApiImplBase(context), NativeSyncApi { + override fun shouldFullSync(): Boolean { + return true + } + + // No-op for Android 10 and below + override fun checkpointSync() { + // Cannot throw exception as this is called from the Dart side + // during the full sync process as well + } + + override fun clearSyncCheckpoint() { + // No-op for Android 10 and below + } + + override fun getMediaChanges(): SyncDelta { + throw IllegalStateException("Method not supported on this Android version.") + } +} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImpl30.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImpl30.kt new file mode 100644 index 00000000000..052032e143a --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImpl30.kt @@ -0,0 +1,89 @@ +package app.alextran.immich.sync + +import android.content.Context +import android.os.Build +import android.provider.MediaStore +import androidx.annotation.RequiresApi +import androidx.annotation.RequiresExtension +import kotlinx.serialization.json.Json + +@RequiresApi(Build.VERSION_CODES.Q) +@RequiresExtension(extension = Build.VERSION_CODES.R, version = 1) +class NativeSyncApiImpl30(context: Context) : NativeSyncApiImplBase(context), NativeSyncApi { + private val ctx: Context = context.applicationContext + private val prefs = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) + + companion object { + const val SHARED_PREF_NAME = "Immich::MediaManager" + const val SHARED_PREF_MEDIA_STORE_VERSION_KEY = "MediaStore::getVersion" + const val SHARED_PREF_MEDIA_STORE_GEN_KEY = "MediaStore::getGeneration" + } + + private fun getSavedGenerationMap(): Map { + return prefs.getString(SHARED_PREF_MEDIA_STORE_GEN_KEY, null)?.let { + Json.decodeFromString>(it) + } ?: emptyMap() + } + + override fun clearSyncCheckpoint() { + prefs.edit().apply { + remove(SHARED_PREF_MEDIA_STORE_VERSION_KEY) + remove(SHARED_PREF_MEDIA_STORE_GEN_KEY) + apply() + } + } + + override fun shouldFullSync(): Boolean = + MediaStore.getVersion(ctx) != prefs.getString(SHARED_PREF_MEDIA_STORE_VERSION_KEY, null) + + override fun checkpointSync() { + val genMap = MediaStore.getExternalVolumeNames(ctx) + .associateWith { MediaStore.getGeneration(ctx, it) } + + prefs.edit().apply { + putString(SHARED_PREF_MEDIA_STORE_VERSION_KEY, MediaStore.getVersion(ctx)) + putString(SHARED_PREF_MEDIA_STORE_GEN_KEY, Json.encodeToString(genMap)) + apply() + } + } + + override fun getMediaChanges(): SyncDelta { + val genMap = getSavedGenerationMap() + val currentVolumes = MediaStore.getExternalVolumeNames(ctx) + val changed = mutableListOf() + val deleted = mutableListOf() + val assetAlbums = mutableMapOf>() + var hasChanges = genMap.keys != currentVolumes + + for (volume in currentVolumes) { + val currentGen = MediaStore.getGeneration(ctx, volume) + val storedGen = genMap[volume] ?: 0 + if (currentGen <= storedGen) { + continue + } + + hasChanges = true + + val selection = + "$MEDIA_SELECTION AND (${MediaStore.MediaColumns.GENERATION_MODIFIED} > ? OR ${MediaStore.MediaColumns.GENERATION_ADDED} > ?)" + val selectionArgs = arrayOf( + *MEDIA_SELECTION_ARGS, + storedGen.toString(), + storedGen.toString() + ) + + getAssets(getCursor(volume, selection, selectionArgs)).forEach { + when (it) { + is AssetResult.ValidAsset -> { + changed.add(it.asset) + assetAlbums[it.asset.id] = listOf(it.albumId) + } + + is AssetResult.InvalidAsset -> deleted.add(it.assetId) + } + } + } + // Unmounted volumes are handled in dart when the album is removed + return SyncDelta(hasChanges, changed, deleted, assetAlbums) + } +} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt new file mode 100644 index 00000000000..23228553077 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt @@ -0,0 +1,177 @@ +package app.alextran.immich.sync + +import android.annotation.SuppressLint +import android.content.Context +import android.database.Cursor +import android.provider.MediaStore +import java.io.File + +sealed class AssetResult { + data class ValidAsset(val asset: PlatformAsset, val albumId: String) : AssetResult() + data class InvalidAsset(val assetId: String) : AssetResult() +} + +@SuppressLint("InlinedApi") +open class NativeSyncApiImplBase(context: Context) { + private val ctx: Context = context.applicationContext + + companion object { + const val MEDIA_SELECTION = + "(${MediaStore.Files.FileColumns.MEDIA_TYPE} = ? OR ${MediaStore.Files.FileColumns.MEDIA_TYPE} = ?)" + val MEDIA_SELECTION_ARGS = arrayOf( + MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE.toString(), + MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO.toString() + ) + const val BUCKET_SELECTION = "(${MediaStore.Files.FileColumns.BUCKET_ID} = ?)" + val ASSET_PROJECTION = arrayOf( + MediaStore.MediaColumns._ID, + MediaStore.MediaColumns.DATA, + MediaStore.MediaColumns.DISPLAY_NAME, + MediaStore.MediaColumns.DATE_TAKEN, + MediaStore.MediaColumns.DATE_ADDED, + MediaStore.MediaColumns.DATE_MODIFIED, + MediaStore.Files.FileColumns.MEDIA_TYPE, + MediaStore.MediaColumns.BUCKET_ID, + MediaStore.MediaColumns.DURATION + ) + } + + protected fun getCursor( + volume: String, + selection: String, + selectionArgs: Array, + projection: Array = ASSET_PROJECTION, + sortOrder: String? = null + ): Cursor? = ctx.contentResolver.query( + MediaStore.Files.getContentUri(volume), + projection, + selection, + selectionArgs, + sortOrder, + ) + + protected fun getAssets(cursor: Cursor?): Sequence { + return sequence { + cursor?.use { c -> + val idColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns._ID) + val dataColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA) + val nameColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME) + val dateTakenColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_TAKEN) + val dateAddedColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_ADDED) + val dateModifiedColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED) + val mediaTypeColumn = c.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE) + val bucketIdColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.BUCKET_ID) + val durationColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DURATION) + + while (c.moveToNext()) { + val id = c.getLong(idColumn).toString() + + val path = c.getString(dataColumn) + if (path.isNullOrBlank() || !File(path).exists()) { + yield(AssetResult.InvalidAsset(id)) + continue + } + + val mediaType = c.getInt(mediaTypeColumn) + val name = c.getString(nameColumn) + // Date taken is milliseconds since epoch, Date added is seconds since epoch + val createdAt = (c.getLong(dateTakenColumn).takeIf { it > 0 }?.div(1000)) + ?: c.getLong(dateAddedColumn) + // Date modified is seconds since epoch + val modifiedAt = c.getLong(dateModifiedColumn) + // Duration is milliseconds + val duration = if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0 + else c.getLong(durationColumn) / 1000 + val bucketId = c.getString(bucketIdColumn) + + val asset = PlatformAsset(id, name, mediaType.toLong(), createdAt, modifiedAt, duration) + yield(AssetResult.ValidAsset(asset, bucketId)) + } + } + } + } + + fun getAlbums(): List { + val albums = mutableListOf() + val albumsCount = mutableMapOf() + + val projection = arrayOf( + MediaStore.Files.FileColumns.BUCKET_ID, + MediaStore.Files.FileColumns.BUCKET_DISPLAY_NAME, + MediaStore.Files.FileColumns.DATE_MODIFIED, + ) + val selection = + "(${MediaStore.Files.FileColumns.BUCKET_ID} IS NOT NULL) AND $MEDIA_SELECTION" + + getCursor( + MediaStore.VOLUME_EXTERNAL, + selection, + MEDIA_SELECTION_ARGS, + projection, + "${MediaStore.Files.FileColumns.DATE_MODIFIED} DESC" + )?.use { cursor -> + val bucketIdColumn = + cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.BUCKET_ID) + val bucketNameColumn = + cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.BUCKET_DISPLAY_NAME) + val dateModified = + cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATE_MODIFIED) + + while (cursor.moveToNext()) { + val id = cursor.getString(bucketIdColumn) + + val count = albumsCount.getOrDefault(id, 0) + if (count != 0) { + albumsCount[id] = count + 1 + continue + } + + val name = cursor.getString(bucketNameColumn) + val updatedAt = cursor.getLong(dateModified) + albums.add(PlatformAlbum(id, name, updatedAt, false, 0)) + albumsCount[id] = 1 + } + } + + return albums.map { it.copy(assetCount = albumsCount[it.id]?.toLong() ?: 0) } + .sortedBy { it.id } + } + + fun getAssetIdsForAlbum(albumId: String): List { + val projection = arrayOf(MediaStore.MediaColumns._ID) + + return getCursor( + MediaStore.VOLUME_EXTERNAL, + "$BUCKET_SELECTION AND $MEDIA_SELECTION", + arrayOf(albumId, *MEDIA_SELECTION_ARGS), + projection + )?.use { cursor -> + val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID) + generateSequence { + if (cursor.moveToNext()) cursor.getLong(idColumn).toString() else null + }.toList() + } ?: emptyList() + } + + fun getAssetsCountSince(albumId: String, timestamp: Long): Long = + getCursor( + MediaStore.VOLUME_EXTERNAL, + "$BUCKET_SELECTION AND ${MediaStore.Files.FileColumns.DATE_ADDED} > ? AND $MEDIA_SELECTION", + arrayOf(albumId, timestamp.toString(), *MEDIA_SELECTION_ARGS), + )?.use { cursor -> cursor.count.toLong() } ?: 0L + + + fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List { + var selection = "$BUCKET_SELECTION AND $MEDIA_SELECTION" + val selectionArgs = mutableListOf(albumId, *MEDIA_SELECTION_ARGS) + + if (updatedTimeCond != null) { + selection += " AND (${MediaStore.Files.FileColumns.DATE_MODIFIED} > ? OR ${MediaStore.Files.FileColumns.DATE_ADDED} > ?)" + selectionArgs.addAll(listOf(updatedTimeCond.toString(), updatedTimeCond.toString())) + } + + return getAssets(getCursor(MediaStore.VOLUME_EXTERNAL, selection, selectionArgs.toTypedArray())) + .mapNotNull { result -> (result as? AssetResult.ValidAsset)?.asset } + .toList() + } +} diff --git a/mobile/android/settings.gradle b/mobile/android/settings.gradle index 74f8904a109..29c3a7c0567 100644 --- a/mobile/android/settings.gradle +++ b/mobile/android/settings.gradle @@ -1,26 +1,27 @@ pluginManagement { - def flutterSdkPath = { - def properties = new Properties() - file("local.properties").withInputStream { properties.load(it) } - def flutterSdkPath = properties.getProperty("flutter.sdk") - assert flutterSdkPath != null, "flutter.sdk not set in local.properties" - return flutterSdkPath - }() + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() - includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") - repositories { - google() - mavenCentral() - gradlePluginPortal() - } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } } plugins { - id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version '8.7.2' apply false - id "org.jetbrains.kotlin.android" version "2.0.20" apply false - id 'com.google.devtools.ksp' version '2.0.20-1.0.24' apply false + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version '8.7.2' apply false + id "org.jetbrains.kotlin.android" version "2.0.20" apply false + id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.22' apply false + id 'com.google.devtools.ksp' version '2.0.20-1.0.24' apply false } include ":app" diff --git a/mobile/immich_lint/pubspec.lock b/mobile/immich_lint/pubspec.lock index 6d4630f1fb2..263a43c22c0 100644 --- a/mobile/immich_lint/pubspec.lock +++ b/mobile/immich_lint/pubspec.lock @@ -5,31 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" + sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57 url: "https://pub.dev" source: hosted - version: "76.0.0" - _macros: - dependency: transitive - description: dart - source: sdk - version: "0.3.3" + version: "80.0.0" analyzer: dependency: "direct main" description: name: analyzer - sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" + sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e" url: "https://pub.dev" source: hosted - version: "6.11.0" + version: "7.3.0" analyzer_plugin: dependency: "direct main" description: name: analyzer_plugin - sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" + sha256: b3075265c5ab222f8b3188342dcb50b476286394a40323e85d1fa725035d40a4 url: "https://pub.dev" source: hosted - version: "0.11.3" + version: "0.13.0" args: dependency: transitive description: @@ -106,34 +101,42 @@ packages: dependency: transitive description: name: custom_lint - sha256: "4500e88854e7581ee43586abeaf4443cb22375d6d289241a87b1aadf678d5545" + sha256: "409c485fd14f544af1da965d5a0d160ee57cd58b63eeaa7280a4f28cf5bda7f1" url: "https://pub.dev" source: hosted - version: "0.6.10" + version: "0.7.5" custom_lint_builder: dependency: "direct main" description: name: custom_lint_builder - sha256: "5a95eff100da256fbf086b329c17c8b49058c261cdf56d3a4157d3c31c511d78" + sha256: "107e0a43606138015777590ee8ce32f26ba7415c25b722ff0908a6f5d7a4c228" url: "https://pub.dev" source: hosted - version: "0.6.10" + version: "0.7.5" custom_lint_core: dependency: transitive description: name: custom_lint_core - sha256: "76a4046cc71d976222a078a8fd4a65e198b70545a8d690a75196dd14f08510f6" + sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be" url: "https://pub.dev" source: hosted - version: "0.6.10" + version: "0.7.5" + custom_lint_visitor: + dependency: transitive + description: + name: custom_lint_visitor + sha256: "36282d85714af494ee2d7da8c8913630aa6694da99f104fb2ed4afcf8fc857d8" + url: "https://pub.dev" + source: hosted + version: "1.0.0+7.3.0" dart_style: dependency: transitive description: name: dart_style - sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" + sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af" url: "https://pub.dev" source: hosted - version: "2.3.8" + version: "3.1.0" file: dependency: transitive description: @@ -154,10 +157,10 @@ packages: dependency: transitive description: name: freezed_annotation - sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "3.0.0" glob: dependency: "direct main" description: @@ -198,14 +201,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - macros: - dependency: transitive - description: - name: macros - sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" - url: "https://pub.dev" - source: hosted - version: "0.1.3-main.0" matcher: dependency: transitive description: @@ -367,4 +362,4 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.6.0 <4.0.0" + dart: ">=3.7.0 <4.0.0" diff --git a/mobile/immich_lint/pubspec.yaml b/mobile/immich_lint/pubspec.yaml index 4cfd8abe819..2890a4a5954 100644 --- a/mobile/immich_lint/pubspec.yaml +++ b/mobile/immich_lint/pubspec.yaml @@ -5,9 +5,9 @@ environment: sdk: '>=3.0.0 <4.0.0' dependencies: - analyzer: ^6.0.0 - analyzer_plugin: ^0.11.3 - custom_lint_builder: ^0.6.4 + analyzer: ^7.0.0 + analyzer_plugin: ^0.13.0 + custom_lint_builder: ^0.7.5 glob: ^2.1.2 dev_dependencies: diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 311f19857b0..3cbbf83f017 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -89,6 +89,16 @@ FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); + path = Sync; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -175,6 +185,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + B2CF7F8C2DDE4EBB00744BF6 /* Sync */, FA9973382CF6DF4B000EF859 /* Runner.entitlements */, 65DD438629917FAD0047FFA8 /* BackgroundSync */, FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */, @@ -224,6 +235,9 @@ dependencies = ( FAC6F8992D287C890078CB2F /* PBXTargetDependency */, ); + fileSystemSynchronizedGroups = ( + B2CF7F8C2DDE4EBB00744BF6 /* Sync */, + ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Immich-Debug.app */; @@ -270,7 +284,6 @@ }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -278,6 +291,7 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + preferredProjectObjectVersion = 77; productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; diff --git a/mobile/ios/Runner/AppDelegate.swift b/mobile/ios/Runner/AppDelegate.swift index fd62618205c..55d08adc6aa 100644 --- a/mobile/ios/Runner/AppDelegate.swift +++ b/mobile/ios/Runner/AppDelegate.swift @@ -22,6 +22,9 @@ import UIKit BackgroundServicePlugin.registerBackgroundProcessing() BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!) + + let controller: FlutterViewController = window?.rootViewController as! FlutterViewController + NativeSyncApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: NativeSyncApiImpl()) BackgroundServicePlugin.setPluginRegistrantCallback { registry in if !registry.hasPlugin("org.cocoapods.path-provider-foundation") { diff --git a/mobile/ios/Runner/Sync/Messages.g.swift b/mobile/ios/Runner/Sync/Messages.g.swift new file mode 100644 index 00000000000..0d7a3026884 --- /dev/null +++ b/mobile/ios/Runner/Sync/Messages.g.swift @@ -0,0 +1,446 @@ +// Autogenerated from Pigeon (v25.3.2), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +import Foundation + +#if os(iOS) + import Flutter +#elseif os(macOS) + import FlutterMacOS +#else + #error("Unsupported platform.") +#endif + +/// Error class for passing custom error details to Dart side. +final class PigeonError: Error { + let code: String + let message: String? + let details: Sendable? + + init(code: String, message: String?, details: Sendable?) { + self.code = code + self.message = message + self.details = details + } + + var localizedDescription: String { + return + "PigeonError(code: \(code), message: \(message ?? ""), details: \(details ?? "")" + } +} + +private func wrapResult(_ result: Any?) -> [Any?] { + return [result] +} + +private func wrapError(_ error: Any) -> [Any?] { + if let pigeonError = error as? PigeonError { + return [ + pigeonError.code, + pigeonError.message, + pigeonError.details, + ] + } + if let flutterError = error as? FlutterError { + return [ + flutterError.code, + flutterError.message, + flutterError.details, + ] + } + return [ + "\(error)", + "\(type(of: error))", + "Stacktrace: \(Thread.callStackSymbols)", + ] +} + +private func isNullish(_ value: Any?) -> Bool { + return value is NSNull || value == nil +} + +private func nilOrValue(_ value: Any?) -> T? { + if value is NSNull { return nil } + return value as! T? +} + +func deepEqualsMessages(_ lhs: Any?, _ rhs: Any?) -> Bool { + let cleanLhs = nilOrValue(lhs) as Any? + let cleanRhs = nilOrValue(rhs) as Any? + switch (cleanLhs, cleanRhs) { + case (nil, nil): + return true + + case (nil, _), (_, nil): + return false + + case is (Void, Void): + return true + + case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable): + return cleanLhsHashable == cleanRhsHashable + + case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]): + guard cleanLhsArray.count == cleanRhsArray.count else { return false } + for (index, element) in cleanLhsArray.enumerated() { + if !deepEqualsMessages(element, cleanRhsArray[index]) { + return false + } + } + return true + + case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): + guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false } + for (key, cleanLhsValue) in cleanLhsDictionary { + guard cleanRhsDictionary.index(forKey: key) != nil else { return false } + if !deepEqualsMessages(cleanLhsValue, cleanRhsDictionary[key]!) { + return false + } + } + return true + + default: + // Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be untrue. + return false + } +} + +func deepHashMessages(value: Any?, hasher: inout Hasher) { + if let valueList = value as? [AnyHashable] { + for item in valueList { deepHashMessages(value: item, hasher: &hasher) } + return + } + + if let valueDict = value as? [AnyHashable: AnyHashable] { + for key in valueDict.keys { + hasher.combine(key) + deepHashMessages(value: valueDict[key]!, hasher: &hasher) + } + return + } + + if let hashableValue = value as? AnyHashable { + hasher.combine(hashableValue.hashValue) + } + + return hasher.combine(String(describing: value)) +} + + + +/// Generated class from Pigeon that represents data sent in messages. +struct PlatformAsset: Hashable { + var id: String + var name: String + var type: Int64 + var createdAt: Int64? = nil + var updatedAt: Int64? = nil + var durationInSeconds: Int64 + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> PlatformAsset? { + let id = pigeonVar_list[0] as! String + let name = pigeonVar_list[1] as! String + let type = pigeonVar_list[2] as! Int64 + let createdAt: Int64? = nilOrValue(pigeonVar_list[3]) + let updatedAt: Int64? = nilOrValue(pigeonVar_list[4]) + let durationInSeconds = pigeonVar_list[5] as! Int64 + + return PlatformAsset( + id: id, + name: name, + type: type, + createdAt: createdAt, + updatedAt: updatedAt, + durationInSeconds: durationInSeconds + ) + } + func toList() -> [Any?] { + return [ + id, + name, + type, + createdAt, + updatedAt, + durationInSeconds, + ] + } + static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool { + return deepEqualsMessages(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashMessages(value: toList(), hasher: &hasher) + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct PlatformAlbum: Hashable { + var id: String + var name: String + var updatedAt: Int64? = nil + var isCloud: Bool + var assetCount: Int64 + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> PlatformAlbum? { + let id = pigeonVar_list[0] as! String + let name = pigeonVar_list[1] as! String + let updatedAt: Int64? = nilOrValue(pigeonVar_list[2]) + let isCloud = pigeonVar_list[3] as! Bool + let assetCount = pigeonVar_list[4] as! Int64 + + return PlatformAlbum( + id: id, + name: name, + updatedAt: updatedAt, + isCloud: isCloud, + assetCount: assetCount + ) + } + func toList() -> [Any?] { + return [ + id, + name, + updatedAt, + isCloud, + assetCount, + ] + } + static func == (lhs: PlatformAlbum, rhs: PlatformAlbum) -> Bool { + return deepEqualsMessages(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashMessages(value: toList(), hasher: &hasher) + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct SyncDelta: Hashable { + var hasChanges: Bool + var updates: [PlatformAsset] + var deletes: [String] + var assetAlbums: [String: [String]] + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> SyncDelta? { + let hasChanges = pigeonVar_list[0] as! Bool + let updates = pigeonVar_list[1] as! [PlatformAsset] + let deletes = pigeonVar_list[2] as! [String] + let assetAlbums = pigeonVar_list[3] as! [String: [String]] + + return SyncDelta( + hasChanges: hasChanges, + updates: updates, + deletes: deletes, + assetAlbums: assetAlbums + ) + } + func toList() -> [Any?] { + return [ + hasChanges, + updates, + deletes, + assetAlbums, + ] + } + static func == (lhs: SyncDelta, rhs: SyncDelta) -> Bool { + return deepEqualsMessages(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashMessages(value: toList(), hasher: &hasher) + } +} + +private class MessagesPigeonCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 129: + return PlatformAsset.fromList(self.readValue() as! [Any?]) + case 130: + return PlatformAlbum.fromList(self.readValue() as! [Any?]) + case 131: + return SyncDelta.fromList(self.readValue() as! [Any?]) + default: + return super.readValue(ofType: type) + } + } +} + +private class MessagesPigeonCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? PlatformAsset { + super.writeByte(129) + super.writeValue(value.toList()) + } else if let value = value as? PlatformAlbum { + super.writeByte(130) + super.writeValue(value.toList()) + } else if let value = value as? SyncDelta { + super.writeByte(131) + super.writeValue(value.toList()) + } else { + super.writeValue(value) + } + } +} + +private class MessagesPigeonCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + return MessagesPigeonCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return MessagesPigeonCodecWriter(data: data) + } +} + +class MessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { + static let shared = MessagesPigeonCodec(readerWriter: MessagesPigeonCodecReaderWriter()) +} + +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol NativeSyncApi { + func shouldFullSync() throws -> Bool + func getMediaChanges() throws -> SyncDelta + func checkpointSync() throws + func clearSyncCheckpoint() throws + func getAssetIdsForAlbum(albumId: String) throws -> [String] + func getAlbums() throws -> [PlatformAlbum] + func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64 + func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset] +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class NativeSyncApiSetup { + static var codec: FlutterStandardMessageCodec { MessagesPigeonCodec.shared } + /// Sets up an instance of `NativeSyncApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: NativeSyncApi?, messageChannelSuffix: String = "") { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + #if os(iOS) + let taskQueue = binaryMessenger.makeBackgroundTaskQueue?() + #else + let taskQueue: FlutterTaskQueue? = nil + #endif + let shouldFullSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + shouldFullSyncChannel.setMessageHandler { _, reply in + do { + let result = try api.shouldFullSync() + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + shouldFullSyncChannel.setMessageHandler(nil) + } + let getMediaChangesChannel = taskQueue == nil + ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) + if let api = api { + getMediaChangesChannel.setMessageHandler { _, reply in + do { + let result = try api.getMediaChanges() + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + getMediaChangesChannel.setMessageHandler(nil) + } + let checkpointSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.checkpointSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + checkpointSyncChannel.setMessageHandler { _, reply in + do { + try api.checkpointSync() + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + checkpointSyncChannel.setMessageHandler(nil) + } + let clearSyncCheckpointChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.clearSyncCheckpoint\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + clearSyncCheckpointChannel.setMessageHandler { _, reply in + do { + try api.clearSyncCheckpoint() + reply(wrapResult(nil)) + } catch { + reply(wrapError(error)) + } + } + } else { + clearSyncCheckpointChannel.setMessageHandler(nil) + } + let getAssetIdsForAlbumChannel = taskQueue == nil + ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) + if let api = api { + getAssetIdsForAlbumChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let albumIdArg = args[0] as! String + do { + let result = try api.getAssetIdsForAlbum(albumId: albumIdArg) + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + getAssetIdsForAlbumChannel.setMessageHandler(nil) + } + let getAlbumsChannel = taskQueue == nil + ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) + if let api = api { + getAlbumsChannel.setMessageHandler { _, reply in + do { + let result = try api.getAlbums() + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + getAlbumsChannel.setMessageHandler(nil) + } + let getAssetsCountSinceChannel = taskQueue == nil + ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) + if let api = api { + getAssetsCountSinceChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let albumIdArg = args[0] as! String + let timestampArg = args[1] as! Int64 + do { + let result = try api.getAssetsCountSince(albumId: albumIdArg, timestamp: timestampArg) + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + getAssetsCountSinceChannel.setMessageHandler(nil) + } + let getAssetsForAlbumChannel = taskQueue == nil + ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) + if let api = api { + getAssetsForAlbumChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let albumIdArg = args[0] as! String + let updatedTimeCondArg: Int64? = nilOrValue(args[1]) + do { + let result = try api.getAssetsForAlbum(albumId: albumIdArg, updatedTimeCond: updatedTimeCondArg) + reply(wrapResult(result)) + } catch { + reply(wrapError(error)) + } + } + } else { + getAssetsForAlbumChannel.setMessageHandler(nil) + } + } +} diff --git a/mobile/ios/Runner/Sync/MessagesImpl.swift b/mobile/ios/Runner/Sync/MessagesImpl.swift new file mode 100644 index 00000000000..5d2f08691d2 --- /dev/null +++ b/mobile/ios/Runner/Sync/MessagesImpl.swift @@ -0,0 +1,246 @@ +import Photos + +struct AssetWrapper: Hashable, Equatable { + let asset: PlatformAsset + + init(with asset: PlatformAsset) { + self.asset = asset + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self.asset.id) + } + + static func == (lhs: AssetWrapper, rhs: AssetWrapper) -> Bool { + return lhs.asset.id == rhs.asset.id + } +} + +extension PHAsset { + func toPlatformAsset() -> PlatformAsset { + return PlatformAsset( + id: localIdentifier, + name: title(), + type: Int64(mediaType.rawValue), + createdAt: creationDate.map { Int64($0.timeIntervalSince1970) }, + updatedAt: modificationDate.map { Int64($0.timeIntervalSince1970) }, + durationInSeconds: Int64(duration) + ) + } +} + +class NativeSyncApiImpl: NativeSyncApi { + private let defaults: UserDefaults + private let changeTokenKey = "immich:changeToken" + private let albumTypes: [PHAssetCollectionType] = [.album, .smartAlbum] + + init(with defaults: UserDefaults = .standard) { + self.defaults = defaults + } + + @available(iOS 16, *) + private func getChangeToken() -> PHPersistentChangeToken? { + guard let data = defaults.data(forKey: changeTokenKey) else { + return nil + } + return try? NSKeyedUnarchiver.unarchivedObject(ofClass: PHPersistentChangeToken.self, from: data) + } + + @available(iOS 16, *) + private func saveChangeToken(token: PHPersistentChangeToken) -> Void { + guard let data = try? NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: true) else { + return + } + defaults.set(data, forKey: changeTokenKey) + } + + func clearSyncCheckpoint() -> Void { + defaults.removeObject(forKey: changeTokenKey) + } + + func checkpointSync() { + guard #available(iOS 16, *) else { + return + } + saveChangeToken(token: PHPhotoLibrary.shared().currentChangeToken) + } + + func shouldFullSync() -> Bool { + guard #available(iOS 16, *), + PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized, + let storedToken = getChangeToken() else { + // When we do not have access to photo library, older iOS version or No token available, fallback to full sync + return true + } + + guard let _ = try? PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken) else { + // Cannot fetch persistent changes + return true + } + + return false + } + + func getAlbums() throws -> [PlatformAlbum] { + var albums: [PlatformAlbum] = [] + + albumTypes.forEach { type in + let collections = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: nil) + collections.enumerateObjects { (album, _, _) in + let options = PHFetchOptions() + options.sortDescriptors = [NSSortDescriptor(key: "modificationDate", ascending: false)] + let assets = PHAsset.fetchAssets(in: album, options: options) + let isCloud = album.assetCollectionSubtype == .albumCloudShared || album.assetCollectionSubtype == .albumMyPhotoStream + + var domainAlbum = PlatformAlbum( + id: album.localIdentifier, + name: album.localizedTitle!, + updatedAt: nil, + isCloud: isCloud, + assetCount: Int64(assets.count) + ) + + if let firstAsset = assets.firstObject { + domainAlbum.updatedAt = firstAsset.modificationDate.map { Int64($0.timeIntervalSince1970) } + } + + albums.append(domainAlbum) + } + } + return albums.sorted { $0.id < $1.id } + } + + func getMediaChanges() throws -> SyncDelta { + guard #available(iOS 16, *) else { + throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature requires iOS 16 or later.", details: nil) + } + + guard PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized else { + throw PigeonError(code: "NO_AUTH", message: "No photo library access", details: nil) + } + + guard let storedToken = getChangeToken() else { + // No token exists, definitely need a full sync + print("MediaManager::getMediaChanges: No token found") + throw PigeonError(code: "NO_TOKEN", message: "No stored change token", details: nil) + } + + let currentToken = PHPhotoLibrary.shared().currentChangeToken + if storedToken == currentToken { + return SyncDelta(hasChanges: false, updates: [], deletes: [], assetAlbums: [:]) + } + + do { + let changes = try PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken) + + var updatedAssets: Set = [] + var deletedAssets: Set = [] + + for change in changes { + guard let details = try? change.changeDetails(for: PHObjectType.asset) else { continue } + + let updated = details.updatedLocalIdentifiers.union(details.insertedLocalIdentifiers) + deletedAssets.formUnion(details.deletedLocalIdentifiers) + + if (updated.isEmpty) { continue } + + let result = PHAsset.fetchAssets(withLocalIdentifiers: Array(updated), options: nil) + for i in 0..) -> [String: [String]] { + guard !assets.isEmpty else { + return [:] + } + + var albumAssets: [String: [String]] = [:] + + for type in albumTypes { + let collections = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: nil) + collections.enumerateObjects { (album, _, _) in + let options = PHFetchOptions() + options.predicate = NSPredicate(format: "localIdentifier IN %@", assets.map(\.id)) + let result = PHAsset.fetchAssets(in: album, options: options) + result.enumerateObjects { (asset, _, _) in + albumAssets[asset.localIdentifier, default: []].append(album.localIdentifier) + } + } + } + return albumAssets + } + + func getAssetIdsForAlbum(albumId: String) throws -> [String] { + let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil) + guard let album = collections.firstObject else { + return [] + } + + var ids: [String] = [] + let assets = PHAsset.fetchAssets(in: album, options: nil) + assets.enumerateObjects { (asset, _, _) in + ids.append(asset.localIdentifier) + } + return ids + } + + func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64 { + let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil) + guard let album = collections.firstObject else { + return 0 + } + + let date = NSDate(timeIntervalSince1970: TimeInterval(timestamp)) + let options = PHFetchOptions() + options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date) + let assets = PHAsset.fetchAssets(in: album, options: options) + return Int64(assets.count) + } + + func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset] { + let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil) + guard let album = collections.firstObject else { + return [] + } + + let options = PHFetchOptions() + if(updatedTimeCond != nil) { + let date = NSDate(timeIntervalSince1970: TimeInterval(updatedTimeCond!)) + options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date) + } + + let result = PHAsset.fetchAssets(in: album, options: options) + if(result.count == 0) { + return [] + } + + var assets: [PlatformAsset] = [] + result.enumerateObjects { (asset, _, _) in + assets.append(asset.toPlatformAsset()) + } + return assets + } +} diff --git a/mobile/lib/constants/constants.dart b/mobile/lib/constants/constants.dart index 33683afd92f..8c95922a3ab 100644 --- a/mobile/lib/constants/constants.dart +++ b/mobile/lib/constants/constants.dart @@ -7,6 +7,7 @@ const int kLogTruncateLimit = 250; // Sync const int kSyncEventBatchSize = 5000; +const int kFetchLocalAssetsBatchSize = 40000; // Hash batch limits const int kBatchHashFileLimit = 128; diff --git a/mobile/lib/domain/interfaces/local_album.interface.dart b/mobile/lib/domain/interfaces/local_album.interface.dart new file mode 100644 index 00000000000..35cfad44552 --- /dev/null +++ b/mobile/lib/domain/interfaces/local_album.interface.dart @@ -0,0 +1,34 @@ +import 'package:immich_mobile/domain/interfaces/db.interface.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/local_album.model.dart'; + +abstract interface class ILocalAlbumRepository implements IDatabaseRepository { + Future> getAll({SortLocalAlbumsBy? sortBy}); + + Future> getAssetsForAlbum(String albumId); + + Future> getAssetIdsForAlbum(String albumId); + + Future upsert( + LocalAlbum album, { + Iterable toUpsert = const [], + Iterable toDelete = const [], + }); + + Future updateAll(Iterable albums); + + Future delete(String albumId); + + Future processDelta({ + required List updates, + required List deletes, + required Map> assetAlbums, + }); + + Future syncAlbumDeletes( + String albumId, + Iterable assetIdsToKeep, + ); +} + +enum SortLocalAlbumsBy { id } diff --git a/mobile/lib/domain/models/asset/asset.model.dart b/mobile/lib/domain/models/asset/asset.model.dart new file mode 100644 index 00000000000..e2bb1fc49f6 --- /dev/null +++ b/mobile/lib/domain/models/asset/asset.model.dart @@ -0,0 +1,47 @@ +part of 'base_asset.model.dart'; + +// Model for an asset stored in the server +class Asset extends BaseAsset { + final String id; + final String? localId; + + const Asset({ + required this.id, + this.localId, + required super.name, + required super.checksum, + required super.type, + required super.createdAt, + required super.updatedAt, + super.width, + super.height, + super.durationInSeconds, + super.isFavorite = false, + }); + + @override + String toString() { + return '''Asset { + id: $id, + name: $name, + type: $type, + createdAt: $createdAt, + updatedAt: $updatedAt, + width: ${width ?? ""}, + height: ${height ?? ""}, + durationInSeconds: ${durationInSeconds ?? ""}, + localId: ${localId ?? ""}, + isFavorite: $isFavorite, + }'''; + } + + @override + bool operator ==(Object other) { + if (other is! Asset) return false; + if (identical(this, other)) return true; + return super == other && id == other.id && localId == other.localId; + } + + @override + int get hashCode => super.hashCode ^ id.hashCode ^ localId.hashCode; +} diff --git a/mobile/lib/domain/models/asset/base_asset.model.dart b/mobile/lib/domain/models/asset/base_asset.model.dart new file mode 100644 index 00000000000..fb954376597 --- /dev/null +++ b/mobile/lib/domain/models/asset/base_asset.model.dart @@ -0,0 +1,76 @@ +part 'asset.model.dart'; +part 'local_asset.model.dart'; + +enum AssetType { + // do not change this order! + other, + image, + video, + audio, +} + +sealed class BaseAsset { + final String name; + final String? checksum; + final AssetType type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final bool isFavorite; + + const BaseAsset({ + required this.name, + required this.checksum, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + this.isFavorite = false, + }); + + @override + String toString() { + return '''BaseAsset { + name: $name, + type: $type, + createdAt: $createdAt, + updatedAt: $updatedAt, + width: ${width ?? ""}, + height: ${height ?? ""}, + durationInSeconds: ${durationInSeconds ?? ""}, + isFavorite: $isFavorite, +}'''; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is BaseAsset) { + return name == other.name && + type == other.type && + createdAt == other.createdAt && + updatedAt == other.updatedAt && + width == other.width && + height == other.height && + durationInSeconds == other.durationInSeconds && + isFavorite == other.isFavorite; + } + return false; + } + + @override + int get hashCode { + return name.hashCode ^ + type.hashCode ^ + createdAt.hashCode ^ + updatedAt.hashCode ^ + width.hashCode ^ + height.hashCode ^ + durationInSeconds.hashCode ^ + isFavorite.hashCode; + } +} diff --git a/mobile/lib/domain/models/asset/local_asset.model.dart b/mobile/lib/domain/models/asset/local_asset.model.dart new file mode 100644 index 00000000000..25e617d8edc --- /dev/null +++ b/mobile/lib/domain/models/asset/local_asset.model.dart @@ -0,0 +1,74 @@ +part of 'base_asset.model.dart'; + +class LocalAsset extends BaseAsset { + final String id; + final String? remoteId; + + const LocalAsset({ + required this.id, + this.remoteId, + required super.name, + super.checksum, + required super.type, + required super.createdAt, + required super.updatedAt, + super.width, + super.height, + super.durationInSeconds, + super.isFavorite = false, + }); + + @override + String toString() { + return '''LocalAsset { + id: $id, + name: $name, + type: $type, + createdAt: $createdAt, + updatedAt: $updatedAt, + width: ${width ?? ""}, + height: ${height ?? ""}, + durationInSeconds: ${durationInSeconds ?? ""}, + remoteId: ${remoteId ?? ""} + isFavorite: $isFavorite, + }'''; + } + + @override + bool operator ==(Object other) { + if (other is! LocalAsset) return false; + if (identical(this, other)) return true; + return super == other && id == other.id && remoteId == other.remoteId; + } + + @override + int get hashCode => super.hashCode ^ id.hashCode ^ remoteId.hashCode; + + LocalAsset copyWith({ + String? id, + String? remoteId, + String? name, + String? checksum, + AssetType? type, + DateTime? createdAt, + DateTime? updatedAt, + int? width, + int? height, + int? durationInSeconds, + bool? isFavorite, + }) { + return LocalAsset( + id: id ?? this.id, + remoteId: remoteId ?? this.remoteId, + name: name ?? this.name, + checksum: checksum ?? this.checksum, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + isFavorite: isFavorite ?? this.isFavorite, + ); + } +} diff --git a/mobile/lib/domain/models/local_album.model.dart b/mobile/lib/domain/models/local_album.model.dart new file mode 100644 index 00000000000..95c56627bbd --- /dev/null +++ b/mobile/lib/domain/models/local_album.model.dart @@ -0,0 +1,70 @@ +enum BackupSelection { + none, + selected, + excluded, +} + +class LocalAlbum { + final String id; + final String name; + final DateTime updatedAt; + + final int assetCount; + final BackupSelection backupSelection; + + const LocalAlbum({ + required this.id, + required this.name, + required this.updatedAt, + this.assetCount = 0, + this.backupSelection = BackupSelection.none, + }); + + LocalAlbum copyWith({ + String? id, + String? name, + DateTime? updatedAt, + int? assetCount, + BackupSelection? backupSelection, + }) { + return LocalAlbum( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + assetCount: assetCount ?? this.assetCount, + backupSelection: backupSelection ?? this.backupSelection, + ); + } + + @override + bool operator ==(Object other) { + if (other is! LocalAlbum) return false; + if (identical(this, other)) return true; + + return other.id == id && + other.name == name && + other.updatedAt == updatedAt && + other.assetCount == assetCount && + other.backupSelection == backupSelection; + } + + @override + int get hashCode { + return id.hashCode ^ + name.hashCode ^ + updatedAt.hashCode ^ + assetCount.hashCode ^ + backupSelection.hashCode; + } + + @override + String toString() { + return '''LocalAlbum: { +id: $id, +name: $name, +updatedAt: $updatedAt, +assetCount: $assetCount, +backupSelection: $backupSelection, +}'''; + } +} diff --git a/mobile/lib/domain/services/local_sync.service.dart b/mobile/lib/domain/services/local_sync.service.dart new file mode 100644 index 00000000000..e07595b6dbd --- /dev/null +++ b/mobile/lib/domain/services/local_sync.service.dart @@ -0,0 +1,379 @@ +import 'dart:async'; + +import 'package:collection/collection.dart'; +import 'package:flutter/widgets.dart'; +import 'package:immich_mobile/domain/interfaces/local_album.interface.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/local_album.model.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/platform/native_sync_api.g.dart'; +import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart'; +import 'package:immich_mobile/utils/diff.dart'; +import 'package:logging/logging.dart'; +import 'package:platform/platform.dart'; + +class LocalSyncService { + final ILocalAlbumRepository _localAlbumRepository; + final NativeSyncApi _nativeSyncApi; + final Platform _platform; + final StoreService _storeService; + final Logger _log = Logger("DeviceSyncService"); + + LocalSyncService({ + required ILocalAlbumRepository localAlbumRepository, + required NativeSyncApi nativeSyncApi, + required StoreService storeService, + Platform? platform, + }) : _localAlbumRepository = localAlbumRepository, + _nativeSyncApi = nativeSyncApi, + _storeService = storeService, + _platform = platform ?? const LocalPlatform(); + + bool get _ignoreIcloudAssets => + _storeService.get(StoreKey.ignoreIcloudAssets, false) == true; + + Future sync({bool full = false}) async { + final Stopwatch stopwatch = Stopwatch()..start(); + try { + if (full || await _nativeSyncApi.shouldFullSync()) { + _log.fine("Full sync request from ${full ? "user" : "native"}"); + DLog.log("Full sync request from ${full ? "user" : "native"}"); + return await fullSync(); + } + + final delta = await _nativeSyncApi.getMediaChanges(); + if (!delta.hasChanges) { + _log.fine("No media changes detected. Skipping sync"); + DLog.log("No media changes detected. Skipping sync"); + return; + } + + DLog.log("Delta updated: ${delta.updates.length}"); + DLog.log("Delta deleted: ${delta.deletes.length}"); + + final deviceAlbums = await _nativeSyncApi.getAlbums(); + await _localAlbumRepository.updateAll(deviceAlbums.toLocalAlbums()); + await _localAlbumRepository.processDelta( + updates: delta.updates.toLocalAssets(), + deletes: delta.deletes, + assetAlbums: delta.assetAlbums, + ); + + final dbAlbums = await _localAlbumRepository.getAll(); + // On Android, we need to sync all albums since it is not possible to + // detect album deletions from the native side + if (_platform.isAndroid) { + for (final album in dbAlbums) { + final deviceIds = await _nativeSyncApi.getAssetIdsForAlbum(album.id); + await _localAlbumRepository.syncAlbumDeletes(album.id, deviceIds); + } + } + + if (_platform.isIOS) { + // On iOS, we need to full sync albums that are marked as cloud as the delta sync + // does not include changes for cloud albums. If ignoreIcloudAssets is enabled, + // remove the albums from the local database from the previous sync + final cloudAlbums = + deviceAlbums.where((a) => a.isCloud).toLocalAlbums(); + for (final album in cloudAlbums) { + final dbAlbum = dbAlbums.firstWhereOrNull((a) => a.id == album.id); + if (dbAlbum == null) { + _log.warning( + "Cloud album ${album.name} not found in local database. Skipping sync.", + ); + continue; + } + if (_ignoreIcloudAssets) { + await removeAlbum(dbAlbum); + } else { + await updateAlbum(dbAlbum, album); + } + } + } + + await _nativeSyncApi.checkpointSync(); + } catch (e, s) { + _log.severe("Error performing device sync", e, s); + } finally { + stopwatch.stop(); + _log.info("Device sync took - ${stopwatch.elapsedMilliseconds}ms"); + DLog.log("Device sync took - ${stopwatch.elapsedMilliseconds}ms"); + } + } + + Future fullSync() async { + try { + final Stopwatch stopwatch = Stopwatch()..start(); + + List deviceAlbums = + List.of(await _nativeSyncApi.getAlbums()); + if (_platform.isIOS && _ignoreIcloudAssets) { + deviceAlbums.removeWhere((album) => album.isCloud); + } + + final dbAlbums = + await _localAlbumRepository.getAll(sortBy: SortLocalAlbumsBy.id); + + await diffSortedLists( + dbAlbums, + deviceAlbums.toLocalAlbums(), + compare: (a, b) => a.id.compareTo(b.id), + both: updateAlbum, + onlyFirst: removeAlbum, + onlySecond: addAlbum, + ); + + await _nativeSyncApi.checkpointSync(); + stopwatch.stop(); + _log.info("Full device sync took - ${stopwatch.elapsedMilliseconds}ms"); + DLog.log("Full device sync took - ${stopwatch.elapsedMilliseconds}ms"); + } catch (e, s) { + _log.severe("Error performing full device sync", e, s); + } + } + + Future addAlbum(LocalAlbum album) async { + try { + _log.fine("Adding device album ${album.name}"); + + final assets = album.assetCount > 0 + ? await _nativeSyncApi.getAssetsForAlbum(album.id) + : []; + + await _localAlbumRepository.upsert( + album, + toUpsert: assets.toLocalAssets(), + ); + _log.fine("Successfully added device album ${album.name}"); + } catch (e, s) { + _log.warning("Error while adding device album", e, s); + } + } + + Future removeAlbum(LocalAlbum a) async { + _log.fine("Removing device album ${a.name}"); + try { + // Asset deletion is handled in the repository + await _localAlbumRepository.delete(a.id); + } catch (e, s) { + _log.warning("Error while removing device album", e, s); + } + } + + // The deviceAlbum is ignored since we are going to refresh it anyways + FutureOr updateAlbum(LocalAlbum dbAlbum, LocalAlbum deviceAlbum) async { + try { + _log.fine("Syncing device album ${dbAlbum.name}"); + + if (_albumsEqual(deviceAlbum, dbAlbum)) { + _log.fine( + "Device album ${dbAlbum.name} has not changed. Skipping sync.", + ); + return false; + } + + _log.fine("Device album ${dbAlbum.name} has changed. Syncing..."); + + // Faster path - only new assets added + if (await checkAddition(dbAlbum, deviceAlbum)) { + _log.fine("Fast synced device album ${dbAlbum.name}"); + DLog.log("Fast synced device album ${dbAlbum.name}"); + return true; + } + + // Slower path - full sync + return await fullDiff(dbAlbum, deviceAlbum); + } catch (e, s) { + _log.warning("Error while diff device album", e, s); + } + return true; + } + + @visibleForTesting + // The [deviceAlbum] is expected to be refreshed before calling this method + // with modified time and asset count + Future checkAddition( + LocalAlbum dbAlbum, + LocalAlbum deviceAlbum, + ) async { + try { + _log.fine("Fast syncing device album ${dbAlbum.name}"); + // Assets has been modified + if (deviceAlbum.assetCount <= dbAlbum.assetCount) { + _log.fine("Local album has modifications. Proceeding to full sync"); + return false; + } + + final updatedTime = + (dbAlbum.updatedAt.millisecondsSinceEpoch ~/ 1000) + 1; + final newAssetsCount = + await _nativeSyncApi.getAssetsCountSince(deviceAlbum.id, updatedTime); + + // Early return if no new assets were found + if (newAssetsCount == 0) { + _log.fine( + "No new assets found despite album having changes. Proceeding to full sync for ${dbAlbum.name}", + ); + return false; + } + + // Check whether there is only addition or if there has been deletions + if (deviceAlbum.assetCount != dbAlbum.assetCount + newAssetsCount) { + _log.fine("Local album has modifications. Proceeding to full sync"); + return false; + } + + final newAssets = await _nativeSyncApi.getAssetsForAlbum( + deviceAlbum.id, + updatedTimeCond: updatedTime, + ); + + await _localAlbumRepository.upsert( + deviceAlbum.copyWith(backupSelection: dbAlbum.backupSelection), + toUpsert: newAssets.toLocalAssets(), + ); + + return true; + } catch (e, s) { + _log.warning("Error on fast syncing local album: ${dbAlbum.name}", e, s); + } + return false; + } + + @visibleForTesting + // The [deviceAlbum] is expected to be refreshed before calling this method + // with modified time and asset count + Future fullDiff(LocalAlbum dbAlbum, LocalAlbum deviceAlbum) async { + try { + final assetsInDevice = deviceAlbum.assetCount > 0 + ? await _nativeSyncApi + .getAssetsForAlbum(deviceAlbum.id) + .then((a) => a.toLocalAssets()) + : []; + final assetsInDb = dbAlbum.assetCount > 0 + ? await _localAlbumRepository.getAssetsForAlbum(dbAlbum.id) + : []; + + if (deviceAlbum.assetCount == 0) { + _log.fine( + "Device album ${deviceAlbum.name} is empty. Removing assets from DB.", + ); + await _localAlbumRepository.upsert( + deviceAlbum.copyWith(backupSelection: dbAlbum.backupSelection), + toDelete: assetsInDb.map((a) => a.id), + ); + return true; + } + + final updatedDeviceAlbum = deviceAlbum.copyWith( + backupSelection: dbAlbum.backupSelection, + ); + + if (dbAlbum.assetCount == 0) { + _log.fine( + "Device album ${deviceAlbum.name} is empty. Adding assets to DB.", + ); + await _localAlbumRepository.upsert( + updatedDeviceAlbum, + toUpsert: assetsInDevice, + ); + return true; + } + + assert(assetsInDb.isSortedBy((a) => a.id)); + assetsInDevice.sort((a, b) => a.id.compareTo(b.id)); + + final assetsToUpsert = []; + final assetsToDelete = []; + + diffSortedListsSync( + assetsInDb, + assetsInDevice, + compare: (a, b) => a.id.compareTo(b.id), + both: (dbAsset, deviceAsset) { + // Custom comparison to check if the asset has been modified without + // comparing the checksum + if (!_assetsEqual(dbAsset, deviceAsset)) { + assetsToUpsert.add(deviceAsset); + return true; + } + return false; + }, + onlyFirst: (dbAsset) => assetsToDelete.add(dbAsset.id), + onlySecond: (deviceAsset) => assetsToUpsert.add(deviceAsset), + ); + + _log.fine( + "Syncing ${deviceAlbum.name}. ${assetsToUpsert.length} assets to add/update and ${assetsToDelete.length} assets to delete", + ); + + if (assetsToUpsert.isEmpty && assetsToDelete.isEmpty) { + _log.fine( + "No asset changes detected in album ${deviceAlbum.name}. Updating metadata.", + ); + _localAlbumRepository.upsert(updatedDeviceAlbum); + return true; + } + + await _localAlbumRepository.upsert( + updatedDeviceAlbum, + toUpsert: assetsToUpsert, + toDelete: assetsToDelete, + ); + + return true; + } catch (e, s) { + _log.warning("Error on full syncing local album: ${dbAlbum.name}", e, s); + } + return true; + } + + bool _assetsEqual(LocalAsset a, LocalAsset b) { + return a.updatedAt.isAtSameMomentAs(b.updatedAt) && + a.createdAt.isAtSameMomentAs(b.createdAt) && + a.width == b.width && + a.height == b.height && + a.durationInSeconds == b.durationInSeconds; + } + + bool _albumsEqual(LocalAlbum a, LocalAlbum b) { + return a.name == b.name && + a.assetCount == b.assetCount && + a.updatedAt.isAtSameMomentAs(b.updatedAt); + } +} + +extension on Iterable { + List toLocalAlbums() { + return map( + (e) => LocalAlbum( + id: e.id, + name: e.name, + updatedAt: e.updatedAt == null + ? DateTime.now() + : DateTime.fromMillisecondsSinceEpoch(e.updatedAt! * 1000), + assetCount: e.assetCount, + ), + ).toList(); + } +} + +extension on Iterable { + List toLocalAssets() { + return map( + (e) => LocalAsset( + id: e.id, + name: e.name, + type: AssetType.values.elementAtOrNull(e.type) ?? AssetType.other, + createdAt: e.createdAt == null + ? DateTime.now() + : DateTime.fromMillisecondsSinceEpoch(e.createdAt! * 1000), + updatedAt: e.updatedAt == null + ? DateTime.now() + : DateTime.fromMillisecondsSinceEpoch(e.updatedAt! * 1000), + durationInSeconds: e.durationInSeconds, + ), + ).toList(); + } +} diff --git a/mobile/lib/domain/utils/background_sync.dart b/mobile/lib/domain/utils/background_sync.dart index f63dc81ba99..6a694ee44ac 100644 --- a/mobile/lib/domain/utils/background_sync.dart +++ b/mobile/lib/domain/utils/background_sync.dart @@ -1,13 +1,12 @@ -// ignore_for_file: avoid-passing-async-when-sync-expected - import 'dart:async'; -import 'package:immich_mobile/providers/infrastructure/sync_stream.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/sync.provider.dart'; import 'package:immich_mobile/utils/isolate.dart'; import 'package:worker_manager/worker_manager.dart'; class BackgroundSyncManager { Cancelable? _syncTask; + Cancelable? _deviceAlbumSyncTask; BackgroundSyncManager(); @@ -23,7 +22,30 @@ class BackgroundSyncManager { return Future.wait(futures); } - Future sync() { + // No need to cancel the task, as it can also be run when the user logs out + Future syncLocal({bool full = false}) { + if (_deviceAlbumSyncTask != null) { + return _deviceAlbumSyncTask!.future; + } + + // We use a ternary operator to avoid [_deviceAlbumSyncTask] from being + // captured by the closure passed to [runInIsolateGentle]. + _deviceAlbumSyncTask = full + ? runInIsolateGentle( + computation: (ref) => + ref.read(localSyncServiceProvider).sync(full: true), + ) + : runInIsolateGentle( + computation: (ref) => + ref.read(localSyncServiceProvider).sync(full: false), + ); + + return _deviceAlbumSyncTask!.whenComplete(() { + _deviceAlbumSyncTask = null; + }); + } + + Future syncRemote() { if (_syncTask != null) { return _syncTask!.future; } @@ -31,9 +53,8 @@ class BackgroundSyncManager { _syncTask = runInIsolateGentle( computation: (ref) => ref.read(syncStreamServiceProvider).sync(), ); - _syncTask!.whenComplete(() { + return _syncTask!.whenComplete(() { _syncTask = null; }); - return _syncTask!.future; } } diff --git a/mobile/lib/infrastructure/entities/local_album.entity.dart b/mobile/lib/infrastructure/entities/local_album.entity.dart new file mode 100644 index 00000000000..74c3e7a8f73 --- /dev/null +++ b/mobile/lib/infrastructure/entities/local_album.entity.dart @@ -0,0 +1,18 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/local_album.model.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class LocalAlbumEntity extends Table with DriftDefaultsMixin { + const LocalAlbumEntity(); + + TextColumn get id => text()(); + TextColumn get name => text()(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + IntColumn get backupSelection => intEnum()(); + + // Used for mark & sweep + BoolColumn get marker_ => boolean().nullable()(); + + @override + Set get primaryKey => {id}; +} diff --git a/mobile/lib/infrastructure/entities/local_album.entity.drift.dart b/mobile/lib/infrastructure/entities/local_album.entity.drift.dart new file mode 100644 index 00000000000..5955742ec0e --- /dev/null +++ b/mobile/lib/infrastructure/entities/local_album.entity.drift.dart @@ -0,0 +1,497 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart' + as i1; +import 'package:immich_mobile/domain/models/local_album.model.dart' as i2; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart' + as i3; +import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4; + +typedef $$LocalAlbumEntityTableCreateCompanionBuilder + = i1.LocalAlbumEntityCompanion Function({ + required String id, + required String name, + i0.Value updatedAt, + required i2.BackupSelection backupSelection, + i0.Value marker_, +}); +typedef $$LocalAlbumEntityTableUpdateCompanionBuilder + = i1.LocalAlbumEntityCompanion Function({ + i0.Value id, + i0.Value name, + i0.Value updatedAt, + i0.Value backupSelection, + i0.Value marker_, +}); + +class $$LocalAlbumEntityTableFilterComposer + extends i0.Composer { + $$LocalAlbumEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get name => $composableBuilder( + column: $table.name, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnWithTypeConverterFilters + get backupSelection => $composableBuilder( + column: $table.backupSelection, + builder: (column) => i0.ColumnWithTypeConverterFilters(column)); + + i0.ColumnFilters get marker_ => $composableBuilder( + column: $table.marker_, builder: (column) => i0.ColumnFilters(column)); +} + +class $$LocalAlbumEntityTableOrderingComposer + extends i0.Composer { + $$LocalAlbumEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get name => $composableBuilder( + column: $table.name, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get backupSelection => $composableBuilder( + column: $table.backupSelection, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get marker_ => $composableBuilder( + column: $table.marker_, builder: (column) => i0.ColumnOrderings(column)); +} + +class $$LocalAlbumEntityTableAnnotationComposer + extends i0.Composer { + $$LocalAlbumEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + i0.GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + i0.GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + i0.GeneratedColumnWithTypeConverter + get backupSelection => $composableBuilder( + column: $table.backupSelection, builder: (column) => column); + + i0.GeneratedColumn get marker_ => + $composableBuilder(column: $table.marker_, builder: (column) => column); +} + +class $$LocalAlbumEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$LocalAlbumEntityTable, + i1.LocalAlbumEntityData, + i1.$$LocalAlbumEntityTableFilterComposer, + i1.$$LocalAlbumEntityTableOrderingComposer, + i1.$$LocalAlbumEntityTableAnnotationComposer, + $$LocalAlbumEntityTableCreateCompanionBuilder, + $$LocalAlbumEntityTableUpdateCompanionBuilder, + ( + i1.LocalAlbumEntityData, + i0.BaseReferences + ), + i1.LocalAlbumEntityData, + i0.PrefetchHooks Function()> { + $$LocalAlbumEntityTableTableManager( + i0.GeneratedDatabase db, i1.$LocalAlbumEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$LocalAlbumEntityTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => i1 + .$$LocalAlbumEntityTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + i1.$$LocalAlbumEntityTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + i0.Value id = const i0.Value.absent(), + i0.Value name = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + i0.Value backupSelection = + const i0.Value.absent(), + i0.Value marker_ = const i0.Value.absent(), + }) => + i1.LocalAlbumEntityCompanion( + id: id, + name: name, + updatedAt: updatedAt, + backupSelection: backupSelection, + marker_: marker_, + ), + createCompanionCallback: ({ + required String id, + required String name, + i0.Value updatedAt = const i0.Value.absent(), + required i2.BackupSelection backupSelection, + i0.Value marker_ = const i0.Value.absent(), + }) => + i1.LocalAlbumEntityCompanion.insert( + id: id, + name: name, + updatedAt: updatedAt, + backupSelection: backupSelection, + marker_: marker_, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), i0.BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$LocalAlbumEntityTableProcessedTableManager = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$LocalAlbumEntityTable, + i1.LocalAlbumEntityData, + i1.$$LocalAlbumEntityTableFilterComposer, + i1.$$LocalAlbumEntityTableOrderingComposer, + i1.$$LocalAlbumEntityTableAnnotationComposer, + $$LocalAlbumEntityTableCreateCompanionBuilder, + $$LocalAlbumEntityTableUpdateCompanionBuilder, + ( + i1.LocalAlbumEntityData, + i0.BaseReferences + ), + i1.LocalAlbumEntityData, + i0.PrefetchHooks Function()>; + +class $LocalAlbumEntityTable extends i3.LocalAlbumEntity + with i0.TableInfo<$LocalAlbumEntityTable, i1.LocalAlbumEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $LocalAlbumEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id'); + @override + late final i0.GeneratedColumn id = i0.GeneratedColumn( + 'id', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _nameMeta = + const i0.VerificationMeta('name'); + @override + late final i0.GeneratedColumn name = i0.GeneratedColumn( + 'name', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _updatedAtMeta = + const i0.VerificationMeta('updatedAt'); + @override + late final i0.GeneratedColumn updatedAt = + i0.GeneratedColumn('updated_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i4.currentDateAndTime); + @override + late final i0.GeneratedColumnWithTypeConverter + backupSelection = i0.GeneratedColumn( + 'backup_selection', aliasedName, false, + type: i0.DriftSqlType.int, requiredDuringInsert: true) + .withConverter( + i1.$LocalAlbumEntityTable.$converterbackupSelection); + static const i0.VerificationMeta _marker_Meta = + const i0.VerificationMeta('marker_'); + @override + late final i0.GeneratedColumn marker_ = i0.GeneratedColumn( + 'marker', aliasedName, true, + type: i0.DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + i0.GeneratedColumn.constraintIsAlways('CHECK ("marker" IN (0, 1))')); + @override + List get $columns => + [id, name, updatedAt, backupSelection, marker_]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('updated_at')) { + context.handle(_updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); + } + if (data.containsKey('marker')) { + context.handle(_marker_Meta, + marker_.isAcceptableOrUnknown(data['marker']!, _marker_Meta)); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + i1.LocalAlbumEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.LocalAlbumEntityData( + id: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!, + updatedAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + backupSelection: i1.$LocalAlbumEntityTable.$converterbackupSelection + .fromSql(attachedDatabase.typeMapping.read(i0.DriftSqlType.int, + data['${effectivePrefix}backup_selection'])!), + marker_: attachedDatabase.typeMapping + .read(i0.DriftSqlType.bool, data['${effectivePrefix}marker']), + ); + } + + @override + $LocalAlbumEntityTable createAlias(String alias) { + return $LocalAlbumEntityTable(attachedDatabase, alias); + } + + static i0.JsonTypeConverter2 + $converterbackupSelection = + const i0.EnumIndexConverter( + i2.BackupSelection.values); + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumEntityData extends i0.DataClass + implements i0.Insertable { + final String id; + final String name; + final DateTime updatedAt; + final i2.BackupSelection backupSelection; + final bool? marker_; + const LocalAlbumEntityData( + {required this.id, + required this.name, + required this.updatedAt, + required this.backupSelection, + this.marker_}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = i0.Variable(id); + map['name'] = i0.Variable(name); + map['updated_at'] = i0.Variable(updatedAt); + { + map['backup_selection'] = i0.Variable(i1 + .$LocalAlbumEntityTable.$converterbackupSelection + .toSql(backupSelection)); + } + if (!nullToAbsent || marker_ != null) { + map['marker'] = i0.Variable(marker_); + } + return map; + } + + factory LocalAlbumEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return LocalAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + updatedAt: serializer.fromJson(json['updatedAt']), + backupSelection: i1.$LocalAlbumEntityTable.$converterbackupSelection + .fromJson(serializer.fromJson(json['backupSelection'])), + marker_: serializer.fromJson(json['marker_']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'updatedAt': serializer.toJson(updatedAt), + 'backupSelection': serializer.toJson(i1 + .$LocalAlbumEntityTable.$converterbackupSelection + .toJson(backupSelection)), + 'marker_': serializer.toJson(marker_), + }; + } + + i1.LocalAlbumEntityData copyWith( + {String? id, + String? name, + DateTime? updatedAt, + i2.BackupSelection? backupSelection, + i0.Value marker_ = const i0.Value.absent()}) => + i1.LocalAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + marker_: marker_.present ? marker_.value : this.marker_, + ); + LocalAlbumEntityData copyWithCompanion(i1.LocalAlbumEntityCompanion data) { + return LocalAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + backupSelection: data.backupSelection.present + ? data.backupSelection.value + : this.backupSelection, + marker_: data.marker_.present ? data.marker_.value : this.marker_, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, name, updatedAt, backupSelection, marker_); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.LocalAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.updatedAt == this.updatedAt && + other.backupSelection == this.backupSelection && + other.marker_ == this.marker_); +} + +class LocalAlbumEntityCompanion + extends i0.UpdateCompanion { + final i0.Value id; + final i0.Value name; + final i0.Value updatedAt; + final i0.Value backupSelection; + final i0.Value marker_; + const LocalAlbumEntityCompanion({ + this.id = const i0.Value.absent(), + this.name = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + this.backupSelection = const i0.Value.absent(), + this.marker_ = const i0.Value.absent(), + }); + LocalAlbumEntityCompanion.insert({ + required String id, + required String name, + this.updatedAt = const i0.Value.absent(), + required i2.BackupSelection backupSelection, + this.marker_ = const i0.Value.absent(), + }) : id = i0.Value(id), + name = i0.Value(name), + backupSelection = i0.Value(backupSelection); + static i0.Insertable custom({ + i0.Expression? id, + i0.Expression? name, + i0.Expression? updatedAt, + i0.Expression? backupSelection, + i0.Expression? marker_, + }) { + return i0.RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (updatedAt != null) 'updated_at': updatedAt, + if (backupSelection != null) 'backup_selection': backupSelection, + if (marker_ != null) 'marker': marker_, + }); + } + + i1.LocalAlbumEntityCompanion copyWith( + {i0.Value? id, + i0.Value? name, + i0.Value? updatedAt, + i0.Value? backupSelection, + i0.Value? marker_}) { + return i1.LocalAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + backupSelection: backupSelection ?? this.backupSelection, + marker_: marker_ ?? this.marker_, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = i0.Variable(id.value); + } + if (name.present) { + map['name'] = i0.Variable(name.value); + } + if (updatedAt.present) { + map['updated_at'] = i0.Variable(updatedAt.value); + } + if (backupSelection.present) { + map['backup_selection'] = i0.Variable(i1 + .$LocalAlbumEntityTable.$converterbackupSelection + .toSql(backupSelection.value)); + } + if (marker_.present) { + map['marker'] = i0.Variable(marker_.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('backupSelection: $backupSelection, ') + ..write('marker_: $marker_') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/local_album_asset.entity.dart b/mobile/lib/infrastructure/entities/local_album_asset.entity.dart new file mode 100644 index 00000000000..b64b9ec2fbe --- /dev/null +++ b/mobile/lib/infrastructure/entities/local_album_asset.entity.dart @@ -0,0 +1,17 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class LocalAlbumAssetEntity extends Table with DriftDefaultsMixin { + const LocalAlbumAssetEntity(); + + TextColumn get assetId => + text().references(LocalAssetEntity, #id, onDelete: KeyAction.cascade)(); + + TextColumn get albumId => + text().references(LocalAlbumEntity, #id, onDelete: KeyAction.cascade)(); + + @override + Set get primaryKey => {assetId, albumId}; +} diff --git a/mobile/lib/infrastructure/entities/local_album_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/local_album_asset.entity.drift.dart new file mode 100644 index 00000000000..e8f94fa74b4 --- /dev/null +++ b/mobile/lib/infrastructure/entities/local_album_asset.entity.drift.dart @@ -0,0 +1,565 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart' + as i1; +import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart' + as i2; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart' + as i3; +import 'package:drift/internal/modular.dart' as i4; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart' + as i5; + +typedef $$LocalAlbumAssetEntityTableCreateCompanionBuilder + = i1.LocalAlbumAssetEntityCompanion Function({ + required String assetId, + required String albumId, +}); +typedef $$LocalAlbumAssetEntityTableUpdateCompanionBuilder + = i1.LocalAlbumAssetEntityCompanion Function({ + i0.Value assetId, + i0.Value albumId, +}); + +final class $$LocalAlbumAssetEntityTableReferences extends i0.BaseReferences< + i0.GeneratedDatabase, + i1.$LocalAlbumAssetEntityTable, + i1.LocalAlbumAssetEntityData> { + $$LocalAlbumAssetEntityTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static i3.$LocalAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) => + i4.ReadDatabaseContainer(db) + .resultSet('local_asset_entity') + .createAlias(i0.$_aliasNameGenerator( + i4.ReadDatabaseContainer(db) + .resultSet( + 'local_album_asset_entity') + .assetId, + i4.ReadDatabaseContainer(db) + .resultSet('local_asset_entity') + .id)); + + i3.$$LocalAssetEntityTableProcessedTableManager get assetId { + final $_column = $_itemColumn('asset_id')!; + + final manager = i3 + .$$LocalAssetEntityTableTableManager( + $_db, + i4.ReadDatabaseContainer($_db) + .resultSet('local_asset_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_assetIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static i5.$LocalAlbumEntityTable _albumIdTable(i0.GeneratedDatabase db) => + i4.ReadDatabaseContainer(db) + .resultSet('local_album_entity') + .createAlias(i0.$_aliasNameGenerator( + i4.ReadDatabaseContainer(db) + .resultSet( + 'local_album_asset_entity') + .albumId, + i4.ReadDatabaseContainer(db) + .resultSet('local_album_entity') + .id)); + + i5.$$LocalAlbumEntityTableProcessedTableManager get albumId { + final $_column = $_itemColumn('album_id')!; + + final manager = i5 + .$$LocalAlbumEntityTableTableManager( + $_db, + i4.ReadDatabaseContainer($_db) + .resultSet('local_album_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_albumIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$LocalAlbumAssetEntityTableFilterComposer + extends i0.Composer { + $$LocalAlbumAssetEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i3.$$LocalAssetEntityTableFilterComposer get assetId { + final i3.$$LocalAssetEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('local_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$LocalAssetEntityTableFilterComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet('local_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i5.$$LocalAlbumEntityTableFilterComposer get albumId { + final i5.$$LocalAlbumEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.albumId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('local_album_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$LocalAlbumEntityTableFilterComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet('local_album_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$LocalAlbumAssetEntityTableOrderingComposer + extends i0.Composer { + $$LocalAlbumAssetEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i3.$$LocalAssetEntityTableOrderingComposer get assetId { + final i3.$$LocalAssetEntityTableOrderingComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('local_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$LocalAssetEntityTableOrderingComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'local_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i5.$$LocalAlbumEntityTableOrderingComposer get albumId { + final i5.$$LocalAlbumEntityTableOrderingComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.albumId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('local_album_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$LocalAlbumEntityTableOrderingComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'local_album_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$LocalAlbumAssetEntityTableAnnotationComposer + extends i0.Composer { + $$LocalAlbumAssetEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i3.$$LocalAssetEntityTableAnnotationComposer get assetId { + final i3.$$LocalAssetEntityTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('local_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$LocalAssetEntityTableAnnotationComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'local_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i5.$$LocalAlbumEntityTableAnnotationComposer get albumId { + final i5.$$LocalAlbumEntityTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.albumId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('local_album_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$LocalAlbumEntityTableAnnotationComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'local_album_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$LocalAlbumAssetEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$LocalAlbumAssetEntityTable, + i1.LocalAlbumAssetEntityData, + i1.$$LocalAlbumAssetEntityTableFilterComposer, + i1.$$LocalAlbumAssetEntityTableOrderingComposer, + i1.$$LocalAlbumAssetEntityTableAnnotationComposer, + $$LocalAlbumAssetEntityTableCreateCompanionBuilder, + $$LocalAlbumAssetEntityTableUpdateCompanionBuilder, + (i1.LocalAlbumAssetEntityData, i1.$$LocalAlbumAssetEntityTableReferences), + i1.LocalAlbumAssetEntityData, + i0.PrefetchHooks Function({bool assetId, bool albumId})> { + $$LocalAlbumAssetEntityTableTableManager( + i0.GeneratedDatabase db, i1.$LocalAlbumAssetEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$LocalAlbumAssetEntityTableFilterComposer( + $db: db, $table: table), + createOrderingComposer: () => + i1.$$LocalAlbumAssetEntityTableOrderingComposer( + $db: db, $table: table), + createComputedFieldComposer: () => + i1.$$LocalAlbumAssetEntityTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + i0.Value assetId = const i0.Value.absent(), + i0.Value albumId = const i0.Value.absent(), + }) => + i1.LocalAlbumAssetEntityCompanion( + assetId: assetId, + albumId: albumId, + ), + createCompanionCallback: ({ + required String assetId, + required String albumId, + }) => + i1.LocalAlbumAssetEntityCompanion.insert( + assetId: assetId, + albumId: albumId, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + i1.$$LocalAlbumAssetEntityTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({assetId = false, albumId = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends i0.TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (assetId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.assetId, + referencedTable: i1.$$LocalAlbumAssetEntityTableReferences + ._assetIdTable(db), + referencedColumn: i1.$$LocalAlbumAssetEntityTableReferences + ._assetIdTable(db) + .id, + ) as T; + } + if (albumId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.albumId, + referencedTable: i1.$$LocalAlbumAssetEntityTableReferences + ._albumIdTable(db), + referencedColumn: i1.$$LocalAlbumAssetEntityTableReferences + ._albumIdTable(db) + .id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$LocalAlbumAssetEntityTableProcessedTableManager + = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$LocalAlbumAssetEntityTable, + i1.LocalAlbumAssetEntityData, + i1.$$LocalAlbumAssetEntityTableFilterComposer, + i1.$$LocalAlbumAssetEntityTableOrderingComposer, + i1.$$LocalAlbumAssetEntityTableAnnotationComposer, + $$LocalAlbumAssetEntityTableCreateCompanionBuilder, + $$LocalAlbumAssetEntityTableUpdateCompanionBuilder, + ( + i1.LocalAlbumAssetEntityData, + i1.$$LocalAlbumAssetEntityTableReferences + ), + i1.LocalAlbumAssetEntityData, + i0.PrefetchHooks Function({bool assetId, bool albumId})>; + +class $LocalAlbumAssetEntityTable extends i2.LocalAlbumAssetEntity + with + i0 + .TableInfo<$LocalAlbumAssetEntityTable, i1.LocalAlbumAssetEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $LocalAlbumAssetEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _assetIdMeta = + const i0.VerificationMeta('assetId'); + @override + late final i0.GeneratedColumn assetId = i0.GeneratedColumn( + 'asset_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES local_asset_entity (id) ON DELETE CASCADE')); + static const i0.VerificationMeta _albumIdMeta = + const i0.VerificationMeta('albumId'); + @override + late final i0.GeneratedColumn albumId = i0.GeneratedColumn( + 'album_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES local_album_entity (id) ON DELETE CASCADE')); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_asset_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('asset_id')) { + context.handle(_assetIdMeta, + assetId.isAcceptableOrUnknown(data['asset_id']!, _assetIdMeta)); + } else if (isInserting) { + context.missing(_assetIdMeta); + } + if (data.containsKey('album_id')) { + context.handle(_albumIdMeta, + albumId.isAcceptableOrUnknown(data['album_id']!, _albumIdMeta)); + } else if (isInserting) { + context.missing(_albumIdMeta); + } + return context; + } + + @override + Set get $primaryKey => {assetId, albumId}; + @override + i1.LocalAlbumAssetEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.LocalAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + albumId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}album_id'])!, + ); + } + + @override + $LocalAlbumAssetEntityTable createAlias(String alias) { + return $LocalAlbumAssetEntityTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumAssetEntityData extends i0.DataClass + implements i0.Insertable { + final String assetId; + final String albumId; + const LocalAlbumAssetEntityData( + {required this.assetId, required this.albumId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = i0.Variable(assetId); + map['album_id'] = i0.Variable(albumId); + return map; + } + + factory LocalAlbumAssetEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return LocalAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + i1.LocalAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => + i1.LocalAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + LocalAlbumAssetEntityData copyWithCompanion( + i1.LocalAlbumAssetEntityCompanion data) { + return LocalAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.LocalAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId); +} + +class LocalAlbumAssetEntityCompanion + extends i0.UpdateCompanion { + final i0.Value assetId; + final i0.Value albumId; + const LocalAlbumAssetEntityCompanion({ + this.assetId = const i0.Value.absent(), + this.albumId = const i0.Value.absent(), + }); + LocalAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = i0.Value(assetId), + albumId = i0.Value(albumId); + static i0.Insertable custom({ + i0.Expression? assetId, + i0.Expression? albumId, + }) { + return i0.RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + i1.LocalAlbumAssetEntityCompanion copyWith( + {i0.Value? assetId, i0.Value? albumId}) { + return i1.LocalAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = i0.Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = i0.Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/local_asset.entity.dart b/mobile/lib/infrastructure/entities/local_asset.entity.dart new file mode 100644 index 00000000000..724cf532c59 --- /dev/null +++ b/mobile/lib/infrastructure/entities/local_asset.entity.dart @@ -0,0 +1,17 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +@TableIndex(name: 'local_asset_checksum', columns: {#checksum}) +class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin { + const LocalAssetEntity(); + + TextColumn get id => text()(); + TextColumn get checksum => text().nullable()(); + + // Only used during backup to mirror the favorite status of the asset in the server + BoolColumn get isFavorite => boolean().withDefault(const Constant(false))(); + + @override + Set get primaryKey => {id}; +} diff --git a/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart new file mode 100644 index 00000000000..0a4896a4a3a --- /dev/null +++ b/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart @@ -0,0 +1,658 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart' + as i1; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as i2; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart' + as i3; +import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4; + +typedef $$LocalAssetEntityTableCreateCompanionBuilder + = i1.LocalAssetEntityCompanion Function({ + required String name, + required i2.AssetType type, + i0.Value createdAt, + i0.Value updatedAt, + i0.Value durationInSeconds, + required String id, + i0.Value checksum, + i0.Value isFavorite, +}); +typedef $$LocalAssetEntityTableUpdateCompanionBuilder + = i1.LocalAssetEntityCompanion Function({ + i0.Value name, + i0.Value type, + i0.Value createdAt, + i0.Value updatedAt, + i0.Value durationInSeconds, + i0.Value id, + i0.Value checksum, + i0.Value isFavorite, +}); + +class $$LocalAssetEntityTableFilterComposer + extends i0.Composer { + $$LocalAssetEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnFilters get name => $composableBuilder( + column: $table.name, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnWithTypeConverterFilters get type => + $composableBuilder( + column: $table.type, + builder: (column) => i0.ColumnWithTypeConverterFilters(column)); + + i0.ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get durationInSeconds => $composableBuilder( + column: $table.durationInSeconds, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get checksum => $composableBuilder( + column: $table.checksum, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get isFavorite => $composableBuilder( + column: $table.isFavorite, builder: (column) => i0.ColumnFilters(column)); +} + +class $$LocalAssetEntityTableOrderingComposer + extends i0.Composer { + $$LocalAssetEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get name => $composableBuilder( + column: $table.name, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get type => $composableBuilder( + column: $table.type, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get durationInSeconds => $composableBuilder( + column: $table.durationInSeconds, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get checksum => $composableBuilder( + column: $table.checksum, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get isFavorite => $composableBuilder( + column: $table.isFavorite, + builder: (column) => i0.ColumnOrderings(column)); +} + +class $$LocalAssetEntityTableAnnotationComposer + extends i0.Composer { + $$LocalAssetEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + i0.GeneratedColumnWithTypeConverter get type => + $composableBuilder(column: $table.type, builder: (column) => column); + + i0.GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + i0.GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + i0.GeneratedColumn get durationInSeconds => $composableBuilder( + column: $table.durationInSeconds, builder: (column) => column); + + i0.GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + i0.GeneratedColumn get checksum => + $composableBuilder(column: $table.checksum, builder: (column) => column); + + i0.GeneratedColumn get isFavorite => $composableBuilder( + column: $table.isFavorite, builder: (column) => column); +} + +class $$LocalAssetEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$LocalAssetEntityTable, + i1.LocalAssetEntityData, + i1.$$LocalAssetEntityTableFilterComposer, + i1.$$LocalAssetEntityTableOrderingComposer, + i1.$$LocalAssetEntityTableAnnotationComposer, + $$LocalAssetEntityTableCreateCompanionBuilder, + $$LocalAssetEntityTableUpdateCompanionBuilder, + ( + i1.LocalAssetEntityData, + i0.BaseReferences + ), + i1.LocalAssetEntityData, + i0.PrefetchHooks Function()> { + $$LocalAssetEntityTableTableManager( + i0.GeneratedDatabase db, i1.$LocalAssetEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$LocalAssetEntityTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => i1 + .$$LocalAssetEntityTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + i1.$$LocalAssetEntityTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + i0.Value name = const i0.Value.absent(), + i0.Value type = const i0.Value.absent(), + i0.Value createdAt = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + i0.Value durationInSeconds = const i0.Value.absent(), + i0.Value id = const i0.Value.absent(), + i0.Value checksum = const i0.Value.absent(), + i0.Value isFavorite = const i0.Value.absent(), + }) => + i1.LocalAssetEntityCompanion( + name: name, + type: type, + createdAt: createdAt, + updatedAt: updatedAt, + durationInSeconds: durationInSeconds, + id: id, + checksum: checksum, + isFavorite: isFavorite, + ), + createCompanionCallback: ({ + required String name, + required i2.AssetType type, + i0.Value createdAt = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + i0.Value durationInSeconds = const i0.Value.absent(), + required String id, + i0.Value checksum = const i0.Value.absent(), + i0.Value isFavorite = const i0.Value.absent(), + }) => + i1.LocalAssetEntityCompanion.insert( + name: name, + type: type, + createdAt: createdAt, + updatedAt: updatedAt, + durationInSeconds: durationInSeconds, + id: id, + checksum: checksum, + isFavorite: isFavorite, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), i0.BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$LocalAssetEntityTableProcessedTableManager = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$LocalAssetEntityTable, + i1.LocalAssetEntityData, + i1.$$LocalAssetEntityTableFilterComposer, + i1.$$LocalAssetEntityTableOrderingComposer, + i1.$$LocalAssetEntityTableAnnotationComposer, + $$LocalAssetEntityTableCreateCompanionBuilder, + $$LocalAssetEntityTableUpdateCompanionBuilder, + ( + i1.LocalAssetEntityData, + i0.BaseReferences + ), + i1.LocalAssetEntityData, + i0.PrefetchHooks Function()>; +i0.Index get localAssetChecksum => i0.Index('local_asset_checksum', + 'CREATE INDEX local_asset_checksum ON local_asset_entity (checksum)'); + +class $LocalAssetEntityTable extends i3.LocalAssetEntity + with i0.TableInfo<$LocalAssetEntityTable, i1.LocalAssetEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $LocalAssetEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _nameMeta = + const i0.VerificationMeta('name'); + @override + late final i0.GeneratedColumn name = i0.GeneratedColumn( + 'name', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + @override + late final i0.GeneratedColumnWithTypeConverter type = + i0.GeneratedColumn('type', aliasedName, false, + type: i0.DriftSqlType.int, requiredDuringInsert: true) + .withConverter( + i1.$LocalAssetEntityTable.$convertertype); + static const i0.VerificationMeta _createdAtMeta = + const i0.VerificationMeta('createdAt'); + @override + late final i0.GeneratedColumn createdAt = + i0.GeneratedColumn('created_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i4.currentDateAndTime); + static const i0.VerificationMeta _updatedAtMeta = + const i0.VerificationMeta('updatedAt'); + @override + late final i0.GeneratedColumn updatedAt = + i0.GeneratedColumn('updated_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i4.currentDateAndTime); + static const i0.VerificationMeta _durationInSecondsMeta = + const i0.VerificationMeta('durationInSeconds'); + @override + late final i0.GeneratedColumn durationInSeconds = + i0.GeneratedColumn('duration_in_seconds', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id'); + @override + late final i0.GeneratedColumn id = i0.GeneratedColumn( + 'id', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _checksumMeta = + const i0.VerificationMeta('checksum'); + @override + late final i0.GeneratedColumn checksum = i0.GeneratedColumn( + 'checksum', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _isFavoriteMeta = + const i0.VerificationMeta('isFavorite'); + @override + late final i0.GeneratedColumn isFavorite = i0.GeneratedColumn( + 'is_favorite', aliasedName, false, + type: i0.DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))'), + defaultValue: const i4.Constant(false)); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + durationInSeconds, + id, + checksum, + isFavorite + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_asset_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('name')) { + context.handle( + _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + if (data.containsKey('updated_at')) { + context.handle(_updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); + } + if (data.containsKey('duration_in_seconds')) { + context.handle( + _durationInSecondsMeta, + durationInSeconds.isAcceptableOrUnknown( + data['duration_in_seconds']!, _durationInSecondsMeta)); + } + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('checksum')) { + context.handle(_checksumMeta, + checksum.isAcceptableOrUnknown(data['checksum']!, _checksumMeta)); + } + if (data.containsKey('is_favorite')) { + context.handle( + _isFavoriteMeta, + isFavorite.isAcceptableOrUnknown( + data['is_favorite']!, _isFavoriteMeta)); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + i1.LocalAssetEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.LocalAssetEntityData( + name: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!, + type: i1.$LocalAssetEntityTable.$convertertype.fromSql(attachedDatabase + .typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}type'])!), + createdAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + durationInSeconds: attachedDatabase.typeMapping.read( + i0.DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']), + id: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!, + checksum: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}checksum']), + isFavorite: attachedDatabase.typeMapping + .read(i0.DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!, + ); + } + + @override + $LocalAssetEntityTable createAlias(String alias) { + return $LocalAssetEntityTable(attachedDatabase, alias); + } + + static i0.JsonTypeConverter2 $convertertype = + const i0.EnumIndexConverter(i2.AssetType.values); + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAssetEntityData extends i0.DataClass + implements i0.Insertable { + final String name; + final i2.AssetType type; + final DateTime createdAt; + final DateTime updatedAt; + final int? durationInSeconds; + final String id; + final String? checksum; + final bool isFavorite; + const LocalAssetEntityData( + {required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.durationInSeconds, + required this.id, + this.checksum, + required this.isFavorite}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = i0.Variable(name); + { + map['type'] = i0.Variable( + i1.$LocalAssetEntityTable.$convertertype.toSql(type)); + } + map['created_at'] = i0.Variable(createdAt); + map['updated_at'] = i0.Variable(updatedAt); + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = i0.Variable(durationInSeconds); + } + map['id'] = i0.Variable(id); + if (!nullToAbsent || checksum != null) { + map['checksum'] = i0.Variable(checksum); + } + map['is_favorite'] = i0.Variable(isFavorite); + return map; + } + + factory LocalAssetEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return LocalAssetEntityData( + name: serializer.fromJson(json['name']), + type: i1.$LocalAssetEntityTable.$convertertype + .fromJson(serializer.fromJson(json['type'])), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer + .toJson(i1.$LocalAssetEntityTable.$convertertype.toJson(type)), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + }; + } + + i1.LocalAssetEntityData copyWith( + {String? name, + i2.AssetType? type, + DateTime? createdAt, + DateTime? updatedAt, + i0.Value durationInSeconds = const i0.Value.absent(), + String? id, + i0.Value checksum = const i0.Value.absent(), + bool? isFavorite}) => + i1.LocalAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum.present ? checksum.value : this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ); + LocalAssetEntityData copyWithCompanion(i1.LocalAssetEntityCompanion data) { + return LocalAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: + data.isFavorite.present ? data.isFavorite.value : this.isFavorite, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(name, type, createdAt, updatedAt, + durationInSeconds, id, checksum, isFavorite); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.LocalAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite); +} + +class LocalAssetEntityCompanion + extends i0.UpdateCompanion { + final i0.Value name; + final i0.Value type; + final i0.Value createdAt; + final i0.Value updatedAt; + final i0.Value durationInSeconds; + final i0.Value id; + final i0.Value checksum; + final i0.Value isFavorite; + const LocalAssetEntityCompanion({ + this.name = const i0.Value.absent(), + this.type = const i0.Value.absent(), + this.createdAt = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + this.durationInSeconds = const i0.Value.absent(), + this.id = const i0.Value.absent(), + this.checksum = const i0.Value.absent(), + this.isFavorite = const i0.Value.absent(), + }); + LocalAssetEntityCompanion.insert({ + required String name, + required i2.AssetType type, + this.createdAt = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + this.durationInSeconds = const i0.Value.absent(), + required String id, + this.checksum = const i0.Value.absent(), + this.isFavorite = const i0.Value.absent(), + }) : name = i0.Value(name), + type = i0.Value(type), + id = i0.Value(id); + static i0.Insertable custom({ + i0.Expression? name, + i0.Expression? type, + i0.Expression? createdAt, + i0.Expression? updatedAt, + i0.Expression? durationInSeconds, + i0.Expression? id, + i0.Expression? checksum, + i0.Expression? isFavorite, + }) { + return i0.RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + }); + } + + i1.LocalAssetEntityCompanion copyWith( + {i0.Value? name, + i0.Value? type, + i0.Value? createdAt, + i0.Value? updatedAt, + i0.Value? durationInSeconds, + i0.Value? id, + i0.Value? checksum, + i0.Value? isFavorite}) { + return i1.LocalAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = i0.Variable(name.value); + } + if (type.present) { + map['type'] = i0.Variable( + i1.$LocalAssetEntityTable.$convertertype.toSql(type.value)); + } + if (createdAt.present) { + map['created_at'] = i0.Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = i0.Variable(updatedAt.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = i0.Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = i0.Variable(id.value); + } + if (checksum.present) { + map['checksum'] = i0.Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = i0.Variable(isFavorite.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index 997714e1b6b..17fcad76bf5 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -3,6 +3,9 @@ import 'dart:async'; import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart'; import 'package:immich_mobile/domain/interfaces/db.interface.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart'; @@ -25,7 +28,16 @@ class IsarDatabaseRepository implements IDatabaseRepository { Zone.current[_kzoneTxn] == null ? _db.writeTxn(callback) : callback(); } -@DriftDatabase(tables: [UserEntity, UserMetadataEntity, PartnerEntity]) +@DriftDatabase( + tables: [ + UserEntity, + UserMetadataEntity, + PartnerEntity, + LocalAlbumEntity, + LocalAssetEntity, + LocalAlbumAssetEntity, + ], +) class Drift extends $Drift implements IDatabaseRepository { Drift([QueryExecutor? executor]) : super( @@ -42,8 +54,9 @@ class Drift extends $Drift implements IDatabaseRepository { @override MigrationStrategy get migration => MigrationStrategy( beforeOpen: (details) async { - await customStatement('PRAGMA journal_mode = WAL'); await customStatement('PRAGMA foreign_keys = ON'); + await customStatement('PRAGMA synchronous = NORMAL'); + await customStatement('PRAGMA journal_mode = WAL'); }, ); } diff --git a/mobile/lib/infrastructure/repositories/db.repository.drift.dart b/mobile/lib/infrastructure/repositories/db.repository.drift.dart index a4c2b31dcd3..6611eb5c929 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.drift.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.drift.dart @@ -7,6 +7,12 @@ import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift as i2; import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart' as i3; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart' + as i4; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart' + as i5; +import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart' + as i6; abstract class $Drift extends i0.GeneratedDatabase { $Drift(i0.QueryExecutor e) : super(e); @@ -16,12 +22,25 @@ abstract class $Drift extends i0.GeneratedDatabase { i2.$UserMetadataEntityTable(this); late final i3.$PartnerEntityTable partnerEntity = i3.$PartnerEntityTable(this); + late final i4.$LocalAlbumEntityTable localAlbumEntity = + i4.$LocalAlbumEntityTable(this); + late final i5.$LocalAssetEntityTable localAssetEntity = + i5.$LocalAssetEntityTable(this); + late final i6.$LocalAlbumAssetEntityTable localAlbumAssetEntity = + i6.$LocalAlbumAssetEntityTable(this); @override Iterable> get allTables => allSchemaEntities.whereType>(); @override - List get allSchemaEntities => - [userEntity, userMetadataEntity, partnerEntity]; + List get allSchemaEntities => [ + userEntity, + userMetadataEntity, + partnerEntity, + localAlbumEntity, + localAssetEntity, + localAlbumAssetEntity, + i5.localAssetChecksum + ]; @override i0.StreamQueryUpdateRules get streamUpdateRules => const i0.StreamQueryUpdateRules( @@ -48,6 +67,22 @@ abstract class $Drift extends i0.GeneratedDatabase { i0.TableUpdate('partner_entity', kind: i0.UpdateKind.delete), ], ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('local_asset_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('local_album_asset_entity', + kind: i0.UpdateKind.delete), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('local_album_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('local_album_asset_entity', + kind: i0.UpdateKind.delete), + ], + ), ], ); @override @@ -64,4 +99,10 @@ class $DriftManager { i2.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity); i3.$$PartnerEntityTableTableManager get partnerEntity => i3.$$PartnerEntityTableTableManager(_db, _db.partnerEntity); + i4.$$LocalAlbumEntityTableTableManager get localAlbumEntity => + i4.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity); + i5.$$LocalAssetEntityTableTableManager get localAssetEntity => + i5.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity); + i6.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i6 + .$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity); } diff --git a/mobile/lib/infrastructure/repositories/local_album.repository.dart b/mobile/lib/infrastructure/repositories/local_album.repository.dart new file mode 100644 index 00000000000..650b7a1aab1 --- /dev/null +++ b/mobile/lib/infrastructure/repositories/local_album.repository.dart @@ -0,0 +1,366 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/interfaces/local_album.interface.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/local_album.model.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:platform/platform.dart'; + +class DriftLocalAlbumRepository extends DriftDatabaseRepository + implements ILocalAlbumRepository { + final Drift _db; + final Platform _platform; + const DriftLocalAlbumRepository(this._db, {Platform? platform}) + : _platform = platform ?? const LocalPlatform(), + super(_db); + + @override + Future> getAll({SortLocalAlbumsBy? sortBy}) { + final assetCount = _db.localAlbumAssetEntity.assetId.count(); + + final query = _db.localAlbumEntity.select().join([ + leftOuterJoin( + _db.localAlbumAssetEntity, + _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id), + useColumns: false, + ), + ]); + query + ..addColumns([assetCount]) + ..groupBy([_db.localAlbumEntity.id]); + if (sortBy == SortLocalAlbumsBy.id) { + query.orderBy([OrderingTerm.asc(_db.localAlbumEntity.id)]); + } + return query + .map( + (row) => row + .readTable(_db.localAlbumEntity) + .toDto(assetCount: row.read(assetCount) ?? 0), + ) + .get(); + } + + @override + Future delete(String albumId) => transaction(() async { + // Remove all assets that are only in this particular album + // We cannot remove all assets in the album because they might be in other albums in iOS + // That is not the case on Android since asset <-> album has one:one mapping + final assetsToDelete = _platform.isIOS + ? await _getUniqueAssetsInAlbum(albumId) + : await getAssetIdsForAlbum(albumId); + await _deleteAssets(assetsToDelete); + + // All the other assets that are still associated will be unlinked automatically on-cascade + await _db.managers.localAlbumEntity + .filter((a) => a.id.equals(albumId)) + .delete(); + }); + + @override + Future syncAlbumDeletes( + String albumId, + Iterable assetIdsToKeep, + ) async { + if (assetIdsToKeep.isEmpty) { + return Future.value(); + } + + final deleteSmt = _db.localAssetEntity.delete(); + deleteSmt.where((localAsset) { + final subQuery = _db.localAlbumAssetEntity.selectOnly() + ..addColumns([_db.localAlbumAssetEntity.assetId]) + ..join([ + innerJoin( + _db.localAlbumEntity, + _db.localAlbumAssetEntity.albumId + .equalsExp(_db.localAlbumEntity.id), + ), + ]); + subQuery.where( + _db.localAlbumEntity.id.equals(albumId) & + _db.localAlbumAssetEntity.assetId.isNotIn(assetIdsToKeep), + ); + return localAsset.id.isInQuery(subQuery); + }); + await deleteSmt.go(); + } + + @override + Future upsert( + LocalAlbum localAlbum, { + Iterable toUpsert = const [], + Iterable toDelete = const [], + }) { + final companion = LocalAlbumEntityCompanion.insert( + id: localAlbum.id, + name: localAlbum.name, + updatedAt: Value(localAlbum.updatedAt), + backupSelection: localAlbum.backupSelection, + ); + + return _db.transaction(() async { + await _db.localAlbumEntity + .insertOne(companion, onConflict: DoUpdate((_) => companion)); + await _addAssets(localAlbum.id, toUpsert); + await _removeAssets(localAlbum.id, toDelete); + }); + } + + @override + Future updateAll(Iterable albums) { + return _db.transaction(() async { + await _db.localAlbumEntity + .update() + .write(const LocalAlbumEntityCompanion(marker_: Value(true))); + + await _db.batch((batch) { + for (final album in albums) { + final companion = LocalAlbumEntityCompanion.insert( + id: album.id, + name: album.name, + updatedAt: Value(album.updatedAt), + backupSelection: album.backupSelection, + marker_: const Value(null), + ); + + batch.insert( + _db.localAlbumEntity, + companion, + onConflict: DoUpdate((_) => companion), + ); + } + }); + + if (_platform.isAndroid) { + // On Android, an asset can only be in one album + // So, get the albums that are marked for deletion + // and delete all the assets that are in those albums + final deleteSmt = _db.localAssetEntity.delete(); + deleteSmt.where((localAsset) { + final subQuery = _db.localAlbumAssetEntity.selectOnly() + ..addColumns([_db.localAlbumAssetEntity.assetId]) + ..join([ + innerJoin( + _db.localAlbumEntity, + _db.localAlbumAssetEntity.albumId + .equalsExp(_db.localAlbumEntity.id), + ), + ]); + subQuery.where(_db.localAlbumEntity.marker_.isNotNull()); + return localAsset.id.isInQuery(subQuery); + }); + await deleteSmt.go(); + } + + await _db.localAlbumEntity.deleteWhere((f) => f.marker_.isNotNull()); + }); + } + + @override + Future> getAssetsForAlbum(String albumId) { + final query = _db.localAlbumAssetEntity.select().join( + [ + innerJoin( + _db.localAssetEntity, + _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id), + ), + ], + ) + ..where(_db.localAlbumAssetEntity.albumId.equals(albumId)) + ..orderBy([OrderingTerm.asc(_db.localAssetEntity.id)]); + return query + .map((row) => row.readTable(_db.localAssetEntity).toDto()) + .get(); + } + + @override + Future> getAssetIdsForAlbum(String albumId) { + final query = _db.localAlbumAssetEntity.selectOnly() + ..addColumns([_db.localAlbumAssetEntity.assetId]) + ..where(_db.localAlbumAssetEntity.albumId.equals(albumId)); + return query + .map((row) => row.read(_db.localAlbumAssetEntity.assetId)!) + .get(); + } + + @override + Future processDelta({ + required List updates, + required List deletes, + required Map> assetAlbums, + }) { + return _db.transaction(() async { + await _deleteAssets(deletes); + + await _upsertAssets(updates); + // The ugly casting below is required for now because the generated code + // casts the returned values from the platform during decoding them + // and iterating over them causes the type to be List instead of + // List + await _db.batch((batch) async { + assetAlbums.cast>().forEach((assetId, albumIds) { + batch.deleteWhere( + _db.localAlbumAssetEntity, + (f) => + f.albumId.isNotIn(albumIds.cast().nonNulls) & + f.assetId.equals(assetId), + ); + }); + }); + await _db.batch((batch) async { + assetAlbums.cast>().forEach((assetId, albumIds) { + batch.insertAll( + _db.localAlbumAssetEntity, + albumIds.cast().nonNulls.map( + (albumId) => LocalAlbumAssetEntityCompanion.insert( + assetId: assetId, + albumId: albumId, + ), + ), + onConflict: DoNothing(), + ); + }); + }); + }); + } + + Future _addAssets(String albumId, Iterable assets) { + if (assets.isEmpty) { + return Future.value(); + } + return transaction(() async { + await _upsertAssets(assets); + await _db.localAlbumAssetEntity.insertAll( + assets.map( + (a) => LocalAlbumAssetEntityCompanion.insert( + assetId: a.id, + albumId: albumId, + ), + ), + mode: InsertMode.insertOrIgnore, + ); + }); + } + + Future _removeAssets(String albumId, Iterable assetIds) async { + if (assetIds.isEmpty) { + return Future.value(); + } + + if (_platform.isAndroid) { + return _deleteAssets(assetIds); + } + + List assetsToDelete = []; + List assetsToUnLink = []; + + final uniqueAssets = await _getUniqueAssetsInAlbum(albumId); + if (uniqueAssets.isEmpty) { + assetsToUnLink = assetIds.toList(); + } else { + // Delete unique assets and unlink others + final uniqueSet = uniqueAssets.toSet(); + + for (final assetId in assetIds) { + if (uniqueSet.contains(assetId)) { + assetsToDelete.add(assetId); + } else { + assetsToUnLink.add(assetId); + } + } + } + + return transaction(() async { + if (assetsToUnLink.isNotEmpty) { + await _db.batch( + (batch) => batch.deleteWhere( + _db.localAlbumAssetEntity, + (f) => f.assetId.isIn(assetsToUnLink) & f.albumId.equals(albumId), + ), + ); + } + + await _deleteAssets(assetsToDelete); + }); + } + + /// Get all asset ids that are only in this album and not in other albums. + /// This is useful in cases where the album is a smart album or a user-created album, especially on iOS + Future> _getUniqueAssetsInAlbum(String albumId) { + final assetId = _db.localAlbumAssetEntity.assetId; + final query = _db.localAlbumAssetEntity.selectOnly() + ..addColumns([assetId]) + ..groupBy( + [assetId], + having: _db.localAlbumAssetEntity.albumId.count().equals(1) & + _db.localAlbumAssetEntity.albumId.equals(albumId), + ); + + return query.map((row) => row.read(assetId)!).get(); + } + + Future _upsertAssets(Iterable localAssets) { + if (localAssets.isEmpty) { + return Future.value(); + } + + return _db.batch((batch) async { + batch.insertAllOnConflictUpdate( + _db.localAssetEntity, + localAssets.map( + (a) => LocalAssetEntityCompanion.insert( + name: a.name, + type: a.type, + createdAt: Value(a.createdAt), + updatedAt: Value(a.updatedAt), + durationInSeconds: Value.absentIfNull(a.durationInSeconds), + id: a.id, + checksum: Value.absentIfNull(a.checksum), + ), + ), + ); + }); + } + + Future _deleteAssets(Iterable ids) { + if (ids.isEmpty) { + return Future.value(); + } + + return _db.batch( + (batch) => batch.deleteWhere( + _db.localAssetEntity, + (f) => f.id.isIn(ids), + ), + ); + } +} + +extension on LocalAlbumEntityData { + LocalAlbum toDto({int assetCount = 0}) { + return LocalAlbum( + id: id, + name: name, + updatedAt: updatedAt, + assetCount: assetCount, + backupSelection: backupSelection, + ); + } +} + +extension on LocalAssetEntityData { + LocalAsset toDto() { + return LocalAsset( + id: id, + name: name, + checksum: checksum, + type: type, + createdAt: createdAt, + updatedAt: updatedAt, + durationInSeconds: durationInSeconds, + isFavorite: isFavorite, + ); + } +} diff --git a/mobile/lib/infrastructure/utils/asset.mixin.dart b/mobile/lib/infrastructure/utils/asset.mixin.dart new file mode 100644 index 00000000000..86495508268 --- /dev/null +++ b/mobile/lib/infrastructure/utils/asset.mixin.dart @@ -0,0 +1,10 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; + +mixin AssetEntityMixin on Table { + TextColumn get name => text()(); + IntColumn get type => intEnum()(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + IntColumn get durationInSeconds => integer().nullable()(); +} diff --git a/mobile/lib/platform/native_sync_api.g.dart b/mobile/lib/platform/native_sync_api.g.dart new file mode 100644 index 00000000000..c4e4c467d41 --- /dev/null +++ b/mobile/lib/platform/native_sync_api.g.dart @@ -0,0 +1,501 @@ +// Autogenerated from Pigeon (v25.3.2), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +PlatformException _createConnectionError(String channelName) { + return PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); +} + +bool _deepEquals(Object? a, Object? b) { + if (a is List && b is List) { + return a.length == b.length && + a.indexed + .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); + } + if (a is Map && b is Map) { + return a.length == b.length && + a.entries.every((MapEntry entry) => + (b as Map).containsKey(entry.key) && + _deepEquals(entry.value, b[entry.key])); + } + return a == b; +} + +class PlatformAsset { + PlatformAsset({ + required this.id, + required this.name, + required this.type, + this.createdAt, + this.updatedAt, + required this.durationInSeconds, + }); + + String id; + + String name; + + int type; + + int? createdAt; + + int? updatedAt; + + int durationInSeconds; + + List _toList() { + return [ + id, + name, + type, + createdAt, + updatedAt, + durationInSeconds, + ]; + } + + Object encode() { + return _toList(); + } + + static PlatformAsset decode(Object result) { + result as List; + return PlatformAsset( + id: result[0]! as String, + name: result[1]! as String, + type: result[2]! as int, + createdAt: result[3] as int?, + updatedAt: result[4] as int?, + durationInSeconds: result[5]! as int, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! PlatformAsset || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} + +class PlatformAlbum { + PlatformAlbum({ + required this.id, + required this.name, + this.updatedAt, + required this.isCloud, + required this.assetCount, + }); + + String id; + + String name; + + int? updatedAt; + + bool isCloud; + + int assetCount; + + List _toList() { + return [ + id, + name, + updatedAt, + isCloud, + assetCount, + ]; + } + + Object encode() { + return _toList(); + } + + static PlatformAlbum decode(Object result) { + result as List; + return PlatformAlbum( + id: result[0]! as String, + name: result[1]! as String, + updatedAt: result[2] as int?, + isCloud: result[3]! as bool, + assetCount: result[4]! as int, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! PlatformAlbum || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} + +class SyncDelta { + SyncDelta({ + required this.hasChanges, + required this.updates, + required this.deletes, + required this.assetAlbums, + }); + + bool hasChanges; + + List updates; + + List deletes; + + Map> assetAlbums; + + List _toList() { + return [ + hasChanges, + updates, + deletes, + assetAlbums, + ]; + } + + Object encode() { + return _toList(); + } + + static SyncDelta decode(Object result) { + result as List; + return SyncDelta( + hasChanges: result[0]! as bool, + updates: (result[1] as List?)!.cast(), + deletes: (result[2] as List?)!.cast(), + assetAlbums: + (result[3] as Map?)!.cast>(), + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! SyncDelta || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} + +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else if (value is PlatformAsset) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is PlatformAlbum) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is SyncDelta) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 129: + return PlatformAsset.decode(readValue(buffer)!); + case 130: + return PlatformAlbum.decode(readValue(buffer)!); + case 131: + return SyncDelta.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +class NativeSyncApi { + /// Constructor for [NativeSyncApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + NativeSyncApi( + {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = + messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + Future shouldFullSync() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as bool?)!; + } + } + + Future getMediaChanges() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as SyncDelta?)!; + } + } + + Future checkpointSync() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.checkpointSync$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future clearSyncCheckpoint() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.clearSyncCheckpoint$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future> getAssetIdsForAlbum(String albumId) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([albumId]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as List?)!.cast(); + } + } + + Future> getAlbums() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as List?)!.cast(); + } + } + + Future getAssetsCountSince(String albumId, int timestamp) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([albumId, timestamp]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as int?)!; + } + } + + Future> getAssetsForAlbum(String albumId, + {int? updatedTimeCond}) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([albumId, updatedTimeCond]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as List?)!.cast(); + } + } +} diff --git a/mobile/lib/presentation/pages/dev/dev_logger.dart b/mobile/lib/presentation/pages/dev/dev_logger.dart new file mode 100644 index 00000000000..6d179241a48 --- /dev/null +++ b/mobile/lib/presentation/pages/dev/dev_logger.dart @@ -0,0 +1,68 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:immich_mobile/domain/models/log.model.dart'; +import 'package:immich_mobile/infrastructure/entities/log.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; +// ignore: import_rule_isar +import 'package:isar/isar.dart'; + +const kDevLoggerTag = 'DEV'; + +abstract final class DLog { + const DLog(); + + static Stream> watchLog() { + final db = Isar.getInstance(); + if (db == null) { + debugPrint('Isar is not initialized'); + return const Stream.empty(); + } + + return db.loggerMessages + .filter() + .context1EqualTo(kDevLoggerTag) + .sortByCreatedAtDesc() + .watch(fireImmediately: true) + .map((logs) => logs.map((log) => log.toDto()).toList()); + } + + static void clearLog() { + final db = Isar.getInstance(); + if (db == null) { + debugPrint('Isar is not initialized'); + return; + } + + db.writeTxnSync(() { + db.loggerMessages.filter().context1EqualTo(kDevLoggerTag).deleteAllSync(); + }); + } + + static void log(String message, [Object? error, StackTrace? stackTrace]) { + debugPrint('[$kDevLoggerTag] [${DateTime.now()}] $message'); + if (error != null) { + debugPrint('Error: $error'); + } + if (stackTrace != null) { + debugPrint('StackTrace: $stackTrace'); + } + + final isar = Isar.getInstance(); + if (isar == null) { + debugPrint('Isar is not initialized'); + return; + } + + final record = LogMessage( + message: message, + level: LogLevel.info, + createdAt: DateTime.now(), + logger: kDevLoggerTag, + error: error?.toString(), + stack: stackTrace?.toString(), + ); + + unawaited(IsarLogRepository(isar).insert(record)); + } +} diff --git a/mobile/lib/presentation/pages/dev/feat_in_development.page.dart b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart new file mode 100644 index 00000000000..da0bea157fe --- /dev/null +++ b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart @@ -0,0 +1,174 @@ +// ignore_for_file: avoid-local-functions + +import 'dart:async'; + +import 'package:auto_route/auto_route.dart'; +import 'package:drift/drift.dart' hide Column; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart'; +import 'package:immich_mobile/providers/background_sync.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; + +final _features = [ + _Feature( + name: 'Sync Local', + icon: Icons.photo_album_rounded, + onTap: (_, ref) => ref.read(backgroundSyncProvider).syncLocal(), + ), + _Feature( + name: 'Sync Local Full', + icon: Icons.photo_library_rounded, + onTap: (_, ref) => ref.read(backgroundSyncProvider).syncLocal(full: true), + ), + _Feature( + name: 'Sync Remote', + icon: Icons.refresh_rounded, + onTap: (_, ref) => ref.read(backgroundSyncProvider).syncRemote(), + ), + _Feature( + name: 'WAL Checkpoint', + icon: Icons.save_rounded, + onTap: (_, ref) => ref + .read(driftProvider) + .customStatement("pragma wal_checkpoint(truncate)"), + ), + _Feature( + name: 'Clear Delta Checkpoint', + icon: Icons.delete_rounded, + onTap: (_, ref) => ref.read(nativeSyncApiProvider).clearSyncCheckpoint(), + ), + _Feature( + name: 'Clear Local Data', + icon: Icons.delete_forever_rounded, + onTap: (_, ref) async { + final db = ref.read(driftProvider); + await db.localAssetEntity.deleteAll(); + await db.localAlbumEntity.deleteAll(); + await db.localAlbumAssetEntity.deleteAll(); + }, + ), + _Feature( + name: 'Local Media Summary', + icon: Icons.table_chart_rounded, + onTap: (ctx, _) => ctx.pushRoute(const LocalMediaSummaryRoute()), + ), +]; + +@RoutePage() +class FeatInDevPage extends StatelessWidget { + const FeatInDevPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Features in Development'), + centerTitle: true, + ), + body: Column( + children: [ + Flexible( + flex: 1, + child: ListView.builder( + itemBuilder: (_, index) { + final feat = _features[index]; + return Consumer( + builder: (ctx, ref, _) => ListTile( + title: Text(feat.name), + trailing: Icon(feat.icon), + visualDensity: VisualDensity.compact, + onTap: () => unawaited(feat.onTap(ctx, ref)), + ), + ); + }, + itemCount: _features.length, + ), + ), + const Divider(height: 0), + const Flexible(child: _DevLogs()), + ], + ), + ); + } +} + +class _Feature { + const _Feature({ + required this.name, + required this.icon, + required this.onTap, + }); + + final String name; + final IconData icon; + final Future Function(BuildContext, WidgetRef _) onTap; +} + +// ignore: prefer-single-widget-per-file +class _DevLogs extends StatelessWidget { + const _DevLogs(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + actions: [ + IconButton( + onPressed: DLog.clearLog, + icon: Icon( + Icons.delete_outline_rounded, + size: 20.0, + color: context.primaryColor, + semanticLabel: "Clear logs", + ), + ), + ], + centerTitle: true, + ), + body: StreamBuilder( + initialData: [], + stream: DLog.watchLog(), + builder: (_, logMessages) { + return ListView.separated( + itemBuilder: (ctx, index) { + // ignore: avoid-unsafe-collection-methods + final logMessage = logMessages.data![index]; + return ListTile( + title: Text( + logMessage.message, + style: TextStyle( + color: ctx.colorScheme.onSurface, + fontSize: 14.0, + overflow: TextOverflow.ellipsis, + ), + ), + subtitle: Text( + "at ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)}", + style: TextStyle( + color: ctx.colorScheme.onSurfaceSecondary, + fontSize: 12.0, + ), + ), + dense: true, + visualDensity: VisualDensity.compact, + tileColor: Colors.transparent, + minLeadingWidth: 10, + ); + }, + separatorBuilder: (_, index) { + return const Divider(height: 0); + }, + itemCount: logMessages.data?.length ?? 0, + ); + }, + ), + ); + } +} diff --git a/mobile/lib/presentation/pages/dev/local_media_stat.page.dart b/mobile/lib/presentation/pages/dev/local_media_stat.page.dart new file mode 100644 index 00000000000..b42cae84fed --- /dev/null +++ b/mobile/lib/presentation/pages/dev/local_media_stat.page.dart @@ -0,0 +1,125 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/local_album.model.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; + +final _stats = [ + _Stat( + name: 'Local Assets', + load: (db) => db.managers.localAssetEntity.count(), + ), + _Stat( + name: 'Local Albums', + load: (db) => db.managers.localAlbumEntity.count(), + ), +]; + +@RoutePage() +class LocalMediaSummaryPage extends StatelessWidget { + const LocalMediaSummaryPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Local Media Summary')), + body: Consumer( + builder: (ctx, ref, __) { + final db = ref.watch(driftProvider); + final albumsFuture = ref.watch(localAlbumRepository).getAll(); + + return CustomScrollView( + slivers: [ + SliverList.builder( + itemBuilder: (_, index) { + final stat = _stats[index]; + final countFuture = stat.load(db); + return _Summary(name: stat.name, countFuture: countFuture); + }, + itemCount: _stats.length, + ), + SliverToBoxAdapter( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Divider(), + Padding( + padding: const EdgeInsets.only(left: 15), + child: Text( + "Album summary", + style: ctx.textTheme.titleMedium, + ), + ), + ], + ), + ), + FutureBuilder( + future: albumsFuture, + initialData: [], + builder: (_, snap) { + final albums = snap.data!; + if (albums.isEmpty) { + return const SliverToBoxAdapter(child: SizedBox.shrink()); + } + + albums.sortBy((a) => a.name); + return SliverList.builder( + itemBuilder: (_, index) { + final album = albums[index]; + final countFuture = db.managers.localAlbumAssetEntity + .filter((f) => f.albumId.id.equals(album.id)) + .count(); + return _Summary( + name: album.name, + countFuture: countFuture, + ); + }, + itemCount: albums.length, + ); + }, + ), + ], + ); + }, + ), + ); + } +} + +// ignore: prefer-single-widget-per-file +class _Summary extends StatelessWidget { + final String name; + final Future countFuture; + + const _Summary({required this.name, required this.countFuture}); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: countFuture, + builder: (ctx, snapshot) { + final Widget subtitle; + + if (snapshot.connectionState == ConnectionState.waiting) { + subtitle = const CircularProgressIndicator(); + } else if (snapshot.hasError) { + subtitle = const Icon(Icons.error_rounded); + } else { + subtitle = Text('${snapshot.data ?? 0}'); + } + return ListTile(title: Text(name), trailing: subtitle); + }, + ); + } +} + +class _Stat { + const _Stat({required this.name, required this.load}); + + final String name; + final Future Function(Drift _) load; +} diff --git a/mobile/lib/providers/infrastructure/album.provider.dart b/mobile/lib/providers/infrastructure/album.provider.dart new file mode 100644 index 00000000000..cb4aadb8a75 --- /dev/null +++ b/mobile/lib/providers/infrastructure/album.provider.dart @@ -0,0 +1,8 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/interfaces/local_album.interface.dart'; +import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; + +final localAlbumRepository = Provider( + (ref) => DriftLocalAlbumRepository(ref.watch(driftProvider)), +); diff --git a/mobile/lib/providers/infrastructure/platform.provider.dart b/mobile/lib/providers/infrastructure/platform.provider.dart new file mode 100644 index 00000000000..477046d0bf3 --- /dev/null +++ b/mobile/lib/providers/infrastructure/platform.provider.dart @@ -0,0 +1,4 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/platform/native_sync_api.g.dart'; + +final nativeSyncApiProvider = Provider((_) => NativeSyncApi()); diff --git a/mobile/lib/providers/infrastructure/sync_stream.provider.dart b/mobile/lib/providers/infrastructure/sync.provider.dart similarity index 64% rename from mobile/lib/providers/infrastructure/sync_stream.provider.dart rename to mobile/lib/providers/infrastructure/sync.provider.dart index e313982a301..96e470eba26 100644 --- a/mobile/lib/providers/infrastructure/sync_stream.provider.dart +++ b/mobile/lib/providers/infrastructure/sync.provider.dart @@ -1,10 +1,14 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/services/local_sync.service.dart'; import 'package:immich_mobile/domain/services/sync_stream.service.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/providers/api.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/store.provider.dart'; final syncStreamServiceProvider = Provider( (ref) => SyncStreamService( @@ -21,3 +25,11 @@ final syncApiRepositoryProvider = Provider( final syncStreamRepositoryProvider = Provider( (ref) => DriftSyncStreamRepository(ref.watch(driftProvider)), ); + +final localSyncServiceProvider = Provider( + (ref) => LocalSyncService( + localAlbumRepository: ref.watch(localAlbumRepository), + nativeSyncApi: ref.watch(nativeSyncApiProvider), + storeService: ref.watch(storeServiceProvider), + ), +); diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 317ce7cc542..a6e1d89ff38 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -63,6 +63,8 @@ import 'package:immich_mobile/pages/search/person_result.page.dart'; import 'package:immich_mobile/pages/search/recently_taken.page.dart'; import 'package:immich_mobile/pages/search/search.page.dart'; import 'package:immich_mobile/pages/share_intent/share_intent.page.dart'; +import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart'; +import 'package:immich_mobile/presentation/pages/dev/local_media_stat.page.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/routing/auth_guard.dart'; @@ -316,5 +318,13 @@ class AppRouter extends RootStackRouter { page: PinAuthRoute.page, guards: [_authGuard, _duplicateGuard], ), + AutoRoute( + page: FeatInDevRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + AutoRoute( + page: LocalMediaSummaryRoute.page, + guards: [_authGuard, _duplicateGuard], + ), ]; } diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index da488779e6f..57fb8cef809 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -1,3 +1,4 @@ +// dart format width=80 // GENERATED CODE - DO NOT MODIFY BY HAND // ************************************************************************** @@ -13,10 +14,7 @@ part of 'router.dart'; /// [ActivitiesPage] class ActivitiesRoute extends PageRouteInfo { const ActivitiesRoute({List? children}) - : super( - ActivitiesRoute.name, - initialChildren: children, - ); + : super(ActivitiesRoute.name, initialChildren: children); static const String name = 'ActivitiesRoute'; @@ -132,10 +130,7 @@ class AlbumAssetSelectionRouteArgs { /// [AlbumOptionsPage] class AlbumOptionsRoute extends PageRouteInfo { const AlbumOptionsRoute({List? children}) - : super( - AlbumOptionsRoute.name, - initialChildren: children, - ); + : super(AlbumOptionsRoute.name, initialChildren: children); static const String name = 'AlbumOptionsRoute'; @@ -156,10 +151,7 @@ class AlbumPreviewRoute extends PageRouteInfo { List? children, }) : super( AlbumPreviewRoute.name, - args: AlbumPreviewRouteArgs( - key: key, - album: album, - ), + args: AlbumPreviewRouteArgs(key: key, album: album), initialChildren: children, ); @@ -169,19 +161,13 @@ class AlbumPreviewRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs(); - return AlbumPreviewPage( - key: args.key, - album: args.album, - ); + return AlbumPreviewPage(key: args.key, album: args.album); }, ); } class AlbumPreviewRouteArgs { - const AlbumPreviewRouteArgs({ - this.key, - required this.album, - }); + const AlbumPreviewRouteArgs({this.key, required this.album}); final Key? key; @@ -203,10 +189,7 @@ class AlbumSharedUserSelectionRoute List? children, }) : super( AlbumSharedUserSelectionRoute.name, - args: AlbumSharedUserSelectionRouteArgs( - key: key, - assets: assets, - ), + args: AlbumSharedUserSelectionRouteArgs(key: key, assets: assets), initialChildren: children, ); @@ -216,19 +199,13 @@ class AlbumSharedUserSelectionRoute name, builder: (data) { final args = data.argsAs(); - return AlbumSharedUserSelectionPage( - key: args.key, - assets: args.assets, - ); + return AlbumSharedUserSelectionPage(key: args.key, assets: args.assets); }, ); } class AlbumSharedUserSelectionRouteArgs { - const AlbumSharedUserSelectionRouteArgs({ - this.key, - required this.assets, - }); + const AlbumSharedUserSelectionRouteArgs({this.key, required this.assets}); final Key? key; @@ -249,10 +226,7 @@ class AlbumViewerRoute extends PageRouteInfo { List? children, }) : super( AlbumViewerRoute.name, - args: AlbumViewerRouteArgs( - key: key, - albumId: albumId, - ), + args: AlbumViewerRouteArgs(key: key, albumId: albumId), initialChildren: children, ); @@ -262,19 +236,13 @@ class AlbumViewerRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs(); - return AlbumViewerPage( - key: args.key, - albumId: args.albumId, - ); + return AlbumViewerPage(key: args.key, albumId: args.albumId); }, ); } class AlbumViewerRouteArgs { - const AlbumViewerRouteArgs({ - this.key, - required this.albumId, - }); + const AlbumViewerRouteArgs({this.key, required this.albumId}); final Key? key; @@ -290,10 +258,7 @@ class AlbumViewerRouteArgs { /// [AlbumsPage] class AlbumsRoute extends PageRouteInfo { const AlbumsRoute({List? children}) - : super( - AlbumsRoute.name, - initialChildren: children, - ); + : super(AlbumsRoute.name, initialChildren: children); static const String name = 'AlbumsRoute'; @@ -309,10 +274,7 @@ class AlbumsRoute extends PageRouteInfo { /// [AllMotionPhotosPage] class AllMotionPhotosRoute extends PageRouteInfo { const AllMotionPhotosRoute({List? children}) - : super( - AllMotionPhotosRoute.name, - initialChildren: children, - ); + : super(AllMotionPhotosRoute.name, initialChildren: children); static const String name = 'AllMotionPhotosRoute'; @@ -328,10 +290,7 @@ class AllMotionPhotosRoute extends PageRouteInfo { /// [AllPeoplePage] class AllPeopleRoute extends PageRouteInfo { const AllPeopleRoute({List? children}) - : super( - AllPeopleRoute.name, - initialChildren: children, - ); + : super(AllPeopleRoute.name, initialChildren: children); static const String name = 'AllPeopleRoute'; @@ -347,10 +306,7 @@ class AllPeopleRoute extends PageRouteInfo { /// [AllPlacesPage] class AllPlacesRoute extends PageRouteInfo { const AllPlacesRoute({List? children}) - : super( - AllPlacesRoute.name, - initialChildren: children, - ); + : super(AllPlacesRoute.name, initialChildren: children); static const String name = 'AllPlacesRoute'; @@ -366,10 +322,7 @@ class AllPlacesRoute extends PageRouteInfo { /// [AllVideosPage] class AllVideosRoute extends PageRouteInfo { const AllVideosRoute({List? children}) - : super( - AllVideosRoute.name, - initialChildren: children, - ); + : super(AllVideosRoute.name, initialChildren: children); static const String name = 'AllVideosRoute'; @@ -390,10 +343,7 @@ class AppLogDetailRoute extends PageRouteInfo { List? children, }) : super( AppLogDetailRoute.name, - args: AppLogDetailRouteArgs( - key: key, - logMessage: logMessage, - ), + args: AppLogDetailRouteArgs(key: key, logMessage: logMessage), initialChildren: children, ); @@ -403,19 +353,13 @@ class AppLogDetailRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs(); - return AppLogDetailPage( - key: args.key, - logMessage: args.logMessage, - ); + return AppLogDetailPage(key: args.key, logMessage: args.logMessage); }, ); } class AppLogDetailRouteArgs { - const AppLogDetailRouteArgs({ - this.key, - required this.logMessage, - }); + const AppLogDetailRouteArgs({this.key, required this.logMessage}); final Key? key; @@ -431,10 +375,7 @@ class AppLogDetailRouteArgs { /// [AppLogPage] class AppLogRoute extends PageRouteInfo { const AppLogRoute({List? children}) - : super( - AppLogRoute.name, - initialChildren: children, - ); + : super(AppLogRoute.name, initialChildren: children); static const String name = 'AppLogRoute'; @@ -450,10 +391,7 @@ class AppLogRoute extends PageRouteInfo { /// [ArchivePage] class ArchiveRoute extends PageRouteInfo { const ArchiveRoute({List? children}) - : super( - ArchiveRoute.name, - initialChildren: children, - ); + : super(ArchiveRoute.name, initialChildren: children); static const String name = 'ArchiveRoute'; @@ -469,10 +407,7 @@ class ArchiveRoute extends PageRouteInfo { /// [BackupAlbumSelectionPage] class BackupAlbumSelectionRoute extends PageRouteInfo { const BackupAlbumSelectionRoute({List? children}) - : super( - BackupAlbumSelectionRoute.name, - initialChildren: children, - ); + : super(BackupAlbumSelectionRoute.name, initialChildren: children); static const String name = 'BackupAlbumSelectionRoute'; @@ -488,10 +423,7 @@ class BackupAlbumSelectionRoute extends PageRouteInfo { /// [BackupControllerPage] class BackupControllerRoute extends PageRouteInfo { const BackupControllerRoute({List? children}) - : super( - BackupControllerRoute.name, - initialChildren: children, - ); + : super(BackupControllerRoute.name, initialChildren: children); static const String name = 'BackupControllerRoute'; @@ -507,10 +439,7 @@ class BackupControllerRoute extends PageRouteInfo { /// [BackupOptionsPage] class BackupOptionsRoute extends PageRouteInfo { const BackupOptionsRoute({List? children}) - : super( - BackupOptionsRoute.name, - initialChildren: children, - ); + : super(BackupOptionsRoute.name, initialChildren: children); static const String name = 'BackupOptionsRoute'; @@ -526,10 +455,7 @@ class BackupOptionsRoute extends PageRouteInfo { /// [ChangePasswordPage] class ChangePasswordRoute extends PageRouteInfo { const ChangePasswordRoute({List? children}) - : super( - ChangePasswordRoute.name, - initialChildren: children, - ); + : super(ChangePasswordRoute.name, initialChildren: children); static const String name = 'ChangePasswordRoute'; @@ -550,10 +476,7 @@ class CreateAlbumRoute extends PageRouteInfo { List? children, }) : super( CreateAlbumRoute.name, - args: CreateAlbumRouteArgs( - key: key, - assets: assets, - ), + args: CreateAlbumRouteArgs(key: key, assets: assets), initialChildren: children, ); @@ -563,20 +486,15 @@ class CreateAlbumRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs( - orElse: () => const CreateAlbumRouteArgs()); - return CreateAlbumPage( - key: args.key, - assets: args.assets, + orElse: () => const CreateAlbumRouteArgs(), ); + return CreateAlbumPage(key: args.key, assets: args.assets); }, ); } class CreateAlbumRouteArgs { - const CreateAlbumRouteArgs({ - this.key, - this.assets, - }); + const CreateAlbumRouteArgs({this.key, this.assets}); final Key? key; @@ -598,11 +516,7 @@ class CropImageRoute extends PageRouteInfo { List? children, }) : super( CropImageRoute.name, - args: CropImageRouteArgs( - key: key, - image: image, - asset: asset, - ), + args: CropImageRouteArgs(key: key, image: image, asset: asset), initialChildren: children, ); @@ -612,11 +526,7 @@ class CropImageRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs(); - return CropImagePage( - key: args.key, - image: args.image, - asset: args.asset, - ); + return CropImagePage(key: args.key, image: args.image, asset: args.asset); }, ); } @@ -702,10 +612,7 @@ class EditImageRouteArgs { /// [FailedBackupStatusPage] class FailedBackupStatusRoute extends PageRouteInfo { const FailedBackupStatusRoute({List? children}) - : super( - FailedBackupStatusRoute.name, - initialChildren: children, - ); + : super(FailedBackupStatusRoute.name, initialChildren: children); static const String name = 'FailedBackupStatusRoute'; @@ -721,10 +628,7 @@ class FailedBackupStatusRoute extends PageRouteInfo { /// [FavoritesPage] class FavoritesRoute extends PageRouteInfo { const FavoritesRoute({List? children}) - : super( - FavoritesRoute.name, - initialChildren: children, - ); + : super(FavoritesRoute.name, initialChildren: children); static const String name = 'FavoritesRoute'; @@ -736,6 +640,22 @@ class FavoritesRoute extends PageRouteInfo { ); } +/// generated route for +/// [FeatInDevPage] +class FeatInDevRoute extends PageRouteInfo { + const FeatInDevRoute({List? children}) + : super(FeatInDevRoute.name, initialChildren: children); + + static const String name = 'FeatInDevRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const FeatInDevPage(); + }, + ); +} + /// generated route for /// [FilterImagePage] class FilterImageRoute extends PageRouteInfo { @@ -746,11 +666,7 @@ class FilterImageRoute extends PageRouteInfo { List? children, }) : super( FilterImageRoute.name, - args: FilterImageRouteArgs( - key: key, - image: image, - asset: asset, - ), + args: FilterImageRouteArgs(key: key, image: image, asset: asset), initialChildren: children, ); @@ -797,10 +713,7 @@ class FolderRoute extends PageRouteInfo { List? children, }) : super( FolderRoute.name, - args: FolderRouteArgs( - key: key, - folder: folder, - ), + args: FolderRouteArgs(key: key, folder: folder), initialChildren: children, ); @@ -809,21 +722,16 @@ class FolderRoute extends PageRouteInfo { static PageInfo page = PageInfo( name, builder: (data) { - final args = - data.argsAs(orElse: () => const FolderRouteArgs()); - return FolderPage( - key: args.key, - folder: args.folder, + final args = data.argsAs( + orElse: () => const FolderRouteArgs(), ); + return FolderPage(key: args.key, folder: args.folder); }, ); } class FolderRouteArgs { - const FolderRouteArgs({ - this.key, - this.folder, - }); + const FolderRouteArgs({this.key, this.folder}); final Key? key; @@ -903,10 +811,7 @@ class GalleryViewerRouteArgs { /// [HeaderSettingsPage] class HeaderSettingsRoute extends PageRouteInfo { const HeaderSettingsRoute({List? children}) - : super( - HeaderSettingsRoute.name, - initialChildren: children, - ); + : super(HeaderSettingsRoute.name, initialChildren: children); static const String name = 'HeaderSettingsRoute'; @@ -922,10 +827,7 @@ class HeaderSettingsRoute extends PageRouteInfo { /// [LibraryPage] class LibraryRoute extends PageRouteInfo { const LibraryRoute({List? children}) - : super( - LibraryRoute.name, - initialChildren: children, - ); + : super(LibraryRoute.name, initialChildren: children); static const String name = 'LibraryRoute'; @@ -941,10 +843,7 @@ class LibraryRoute extends PageRouteInfo { /// [LocalAlbumsPage] class LocalAlbumsRoute extends PageRouteInfo { const LocalAlbumsRoute({List? children}) - : super( - LocalAlbumsRoute.name, - initialChildren: children, - ); + : super(LocalAlbumsRoute.name, initialChildren: children); static const String name = 'LocalAlbumsRoute'; @@ -956,14 +855,27 @@ class LocalAlbumsRoute extends PageRouteInfo { ); } +/// generated route for +/// [LocalMediaSummaryPage] +class LocalMediaSummaryRoute extends PageRouteInfo { + const LocalMediaSummaryRoute({List? children}) + : super(LocalMediaSummaryRoute.name, initialChildren: children); + + static const String name = 'LocalMediaSummaryRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const LocalMediaSummaryPage(); + }, + ); +} + /// generated route for /// [LockedPage] class LockedRoute extends PageRouteInfo { const LockedRoute({List? children}) - : super( - LockedRoute.name, - initialChildren: children, - ); + : super(LockedRoute.name, initialChildren: children); static const String name = 'LockedRoute'; @@ -979,10 +891,7 @@ class LockedRoute extends PageRouteInfo { /// [LoginPage] class LoginRoute extends PageRouteInfo { const LoginRoute({List? children}) - : super( - LoginRoute.name, - initialChildren: children, - ); + : super(LoginRoute.name, initialChildren: children); static const String name = 'LoginRoute'; @@ -1016,7 +925,8 @@ class MapLocationPickerRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs( - orElse: () => const MapLocationPickerRouteArgs()); + orElse: () => const MapLocationPickerRouteArgs(), + ); return MapLocationPickerPage( key: args.key, initialLatLng: args.initialLatLng, @@ -1044,16 +954,10 @@ class MapLocationPickerRouteArgs { /// generated route for /// [MapPage] class MapRoute extends PageRouteInfo { - MapRoute({ - Key? key, - LatLng? initialLocation, - List? children, - }) : super( + MapRoute({Key? key, LatLng? initialLocation, List? children}) + : super( MapRoute.name, - args: MapRouteArgs( - key: key, - initialLocation: initialLocation, - ), + args: MapRouteArgs(key: key, initialLocation: initialLocation), initialChildren: children, ); @@ -1062,21 +966,16 @@ class MapRoute extends PageRouteInfo { static PageInfo page = PageInfo( name, builder: (data) { - final args = - data.argsAs(orElse: () => const MapRouteArgs()); - return MapPage( - key: args.key, - initialLocation: args.initialLocation, + final args = data.argsAs( + orElse: () => const MapRouteArgs(), ); + return MapPage(key: args.key, initialLocation: args.initialLocation); }, ); } class MapRouteArgs { - const MapRouteArgs({ - this.key, - this.initialLocation, - }); + const MapRouteArgs({this.key, this.initialLocation}); final Key? key; @@ -1213,10 +1112,7 @@ class PartnerDetailRoute extends PageRouteInfo { List? children, }) : super( PartnerDetailRoute.name, - args: PartnerDetailRouteArgs( - key: key, - partner: partner, - ), + args: PartnerDetailRouteArgs(key: key, partner: partner), initialChildren: children, ); @@ -1226,19 +1122,13 @@ class PartnerDetailRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs(); - return PartnerDetailPage( - key: args.key, - partner: args.partner, - ); + return PartnerDetailPage(key: args.key, partner: args.partner); }, ); } class PartnerDetailRouteArgs { - const PartnerDetailRouteArgs({ - this.key, - required this.partner, - }); + const PartnerDetailRouteArgs({this.key, required this.partner}); final Key? key; @@ -1254,10 +1144,7 @@ class PartnerDetailRouteArgs { /// [PartnerPage] class PartnerRoute extends PageRouteInfo { const PartnerRoute({List? children}) - : super( - PartnerRoute.name, - initialChildren: children, - ); + : super(PartnerRoute.name, initialChildren: children); static const String name = 'PartnerRoute'; @@ -1273,10 +1160,7 @@ class PartnerRoute extends PageRouteInfo { /// [PeopleCollectionPage] class PeopleCollectionRoute extends PageRouteInfo { const PeopleCollectionRoute({List? children}) - : super( - PeopleCollectionRoute.name, - initialChildren: children, - ); + : super(PeopleCollectionRoute.name, initialChildren: children); static const String name = 'PeopleCollectionRoute'; @@ -1292,10 +1176,7 @@ class PeopleCollectionRoute extends PageRouteInfo { /// [PermissionOnboardingPage] class PermissionOnboardingRoute extends PageRouteInfo { const PermissionOnboardingRoute({List? children}) - : super( - PermissionOnboardingRoute.name, - initialChildren: children, - ); + : super(PermissionOnboardingRoute.name, initialChildren: children); static const String name = 'PermissionOnboardingRoute'; @@ -1363,10 +1244,7 @@ class PersonResultRouteArgs { /// [PhotosPage] class PhotosRoute extends PageRouteInfo { const PhotosRoute({List? children}) - : super( - PhotosRoute.name, - initialChildren: children, - ); + : super(PhotosRoute.name, initialChildren: children); static const String name = 'PhotosRoute'; @@ -1387,10 +1265,7 @@ class PinAuthRoute extends PageRouteInfo { List? children, }) : super( PinAuthRoute.name, - args: PinAuthRouteArgs( - key: key, - createPinCode: createPinCode, - ), + args: PinAuthRouteArgs(key: key, createPinCode: createPinCode), initialChildren: children, ); @@ -1399,21 +1274,16 @@ class PinAuthRoute extends PageRouteInfo { static PageInfo page = PageInfo( name, builder: (data) { - final args = - data.argsAs(orElse: () => const PinAuthRouteArgs()); - return PinAuthPage( - key: args.key, - createPinCode: args.createPinCode, + final args = data.argsAs( + orElse: () => const PinAuthRouteArgs(), ); + return PinAuthPage(key: args.key, createPinCode: args.createPinCode); }, ); } class PinAuthRouteArgs { - const PinAuthRouteArgs({ - this.key, - this.createPinCode = false, - }); + const PinAuthRouteArgs({this.key, this.createPinCode = false}); final Key? key; @@ -1447,7 +1317,8 @@ class PlacesCollectionRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs( - orElse: () => const PlacesCollectionRouteArgs()); + orElse: () => const PlacesCollectionRouteArgs(), + ); return PlacesCollectionPage( key: args.key, currentLocation: args.currentLocation, @@ -1457,10 +1328,7 @@ class PlacesCollectionRoute extends PageRouteInfo { } class PlacesCollectionRouteArgs { - const PlacesCollectionRouteArgs({ - this.key, - this.currentLocation, - }); + const PlacesCollectionRouteArgs({this.key, this.currentLocation}); final Key? key; @@ -1476,10 +1344,7 @@ class PlacesCollectionRouteArgs { /// [RecentlyTakenPage] class RecentlyTakenRoute extends PageRouteInfo { const RecentlyTakenRoute({List? children}) - : super( - RecentlyTakenRoute.name, - initialChildren: children, - ); + : super(RecentlyTakenRoute.name, initialChildren: children); static const String name = 'RecentlyTakenRoute'; @@ -1500,10 +1365,7 @@ class SearchRoute extends PageRouteInfo { List? children, }) : super( SearchRoute.name, - args: SearchRouteArgs( - key: key, - prefilter: prefilter, - ), + args: SearchRouteArgs(key: key, prefilter: prefilter), initialChildren: children, ); @@ -1512,21 +1374,16 @@ class SearchRoute extends PageRouteInfo { static PageInfo page = PageInfo( name, builder: (data) { - final args = - data.argsAs(orElse: () => const SearchRouteArgs()); - return SearchPage( - key: args.key, - prefilter: args.prefilter, + final args = data.argsAs( + orElse: () => const SearchRouteArgs(), ); + return SearchPage(key: args.key, prefilter: args.prefilter); }, ); } class SearchRouteArgs { - const SearchRouteArgs({ - this.key, - this.prefilter, - }); + const SearchRouteArgs({this.key, this.prefilter}); final Key? key; @@ -1542,10 +1399,7 @@ class SearchRouteArgs { /// [SettingsPage] class SettingsRoute extends PageRouteInfo { const SettingsRoute({List? children}) - : super( - SettingsRoute.name, - initialChildren: children, - ); + : super(SettingsRoute.name, initialChildren: children); static const String name = 'SettingsRoute'; @@ -1566,10 +1420,7 @@ class SettingsSubRoute extends PageRouteInfo { List? children, }) : super( SettingsSubRoute.name, - args: SettingsSubRouteArgs( - section: section, - key: key, - ), + args: SettingsSubRouteArgs(section: section, key: key), initialChildren: children, ); @@ -1579,19 +1430,13 @@ class SettingsSubRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs(); - return SettingsSubPage( - args.section, - key: args.key, - ); + return SettingsSubPage(args.section, key: args.key); }, ); } class SettingsSubRouteArgs { - const SettingsSubRouteArgs({ - required this.section, - this.key, - }); + const SettingsSubRouteArgs({required this.section, this.key}); final SettingSection section; @@ -1612,10 +1457,7 @@ class ShareIntentRoute extends PageRouteInfo { List? children, }) : super( ShareIntentRoute.name, - args: ShareIntentRouteArgs( - key: key, - attachments: attachments, - ), + args: ShareIntentRouteArgs(key: key, attachments: attachments), initialChildren: children, ); @@ -1625,19 +1467,13 @@ class ShareIntentRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs(); - return ShareIntentPage( - key: args.key, - attachments: args.attachments, - ); + return ShareIntentPage(key: args.key, attachments: args.attachments); }, ); } class ShareIntentRouteArgs { - const ShareIntentRouteArgs({ - this.key, - required this.attachments, - }); + const ShareIntentRouteArgs({this.key, required this.attachments}); final Key? key; @@ -1675,7 +1511,8 @@ class SharedLinkEditRoute extends PageRouteInfo { name, builder: (data) { final args = data.argsAs( - orElse: () => const SharedLinkEditRouteArgs()); + orElse: () => const SharedLinkEditRouteArgs(), + ); return SharedLinkEditPage( key: args.key, existingLink: args.existingLink, @@ -1712,10 +1549,7 @@ class SharedLinkEditRouteArgs { /// [SharedLinkPage] class SharedLinkRoute extends PageRouteInfo { const SharedLinkRoute({List? children}) - : super( - SharedLinkRoute.name, - initialChildren: children, - ); + : super(SharedLinkRoute.name, initialChildren: children); static const String name = 'SharedLinkRoute'; @@ -1731,10 +1565,7 @@ class SharedLinkRoute extends PageRouteInfo { /// [SplashScreenPage] class SplashScreenRoute extends PageRouteInfo { const SplashScreenRoute({List? children}) - : super( - SplashScreenRoute.name, - initialChildren: children, - ); + : super(SplashScreenRoute.name, initialChildren: children); static const String name = 'SplashScreenRoute'; @@ -1750,10 +1581,7 @@ class SplashScreenRoute extends PageRouteInfo { /// [TabControllerPage] class TabControllerRoute extends PageRouteInfo { const TabControllerRoute({List? children}) - : super( - TabControllerRoute.name, - initialChildren: children, - ); + : super(TabControllerRoute.name, initialChildren: children); static const String name = 'TabControllerRoute'; @@ -1769,10 +1597,7 @@ class TabControllerRoute extends PageRouteInfo { /// [TrashPage] class TrashRoute extends PageRouteInfo { const TrashRoute({List? children}) - : super( - TrashRoute.name, - initialChildren: children, - ); + : super(TrashRoute.name, initialChildren: children); static const String name = 'TrashRoute'; diff --git a/mobile/lib/widgets/common/immich_app_bar.dart b/mobile/lib/widgets/common/immich_app_bar.dart index 4f95e657d95..09f81b9e1a1 100644 --- a/mobile/lib/widgets/common/immich_app_bar.dart +++ b/mobile/lib/widgets/common/immich_app_bar.dart @@ -7,7 +7,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/models/server_info/server_info.model.dart'; -import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; @@ -180,10 +179,10 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { child: action, ), ), - if (kDebugMode) + if (kDebugMode || kProfileMode) IconButton( - onPressed: () => ref.read(backgroundSyncProvider).sync(), - icon: const Icon(Icons.sync), + icon: const Icon(Icons.science_rounded), + onPressed: () => context.pushRoute(const FeatInDevRoute()), ), if (showUploadButton) Padding( diff --git a/mobile/makefile b/mobile/makefile index b0083b1495b..b797a659283 100644 --- a/mobile/makefile +++ b/mobile/makefile @@ -1,7 +1,13 @@ -.PHONY: build watch create_app_icon create_splash build_release_android +.PHONY: build watch create_app_icon create_splash build_release_android pigeon build: dart run build_runner build --delete-conflicting-outputs +# Remove once auto_route updated to 10.1.0 + dart format lib/routing/router.gr.dart + +pigeon: + dart run pigeon --input pigeon/native_sync_api.dart + dart format lib/platform/native_sync_api.g.dart watch: dart run build_runner watch --delete-conflicting-outputs @@ -19,4 +25,5 @@ migrations: dart run drift_dev make-migrations translation: - dart run easy_localization:generate -S ../i18n \ No newline at end of file + dart run easy_localization:generate -S ../i18n + dart format lib/generated/codegen_loader.g.dart \ No newline at end of file diff --git a/mobile/pigeon/native_sync_api.dart b/mobile/pigeon/native_sync_api.dart new file mode 100644 index 00000000000..b8a7500d6e5 --- /dev/null +++ b/mobile/pigeon/native_sync_api.dart @@ -0,0 +1,89 @@ +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon( + PigeonOptions( + dartOut: 'lib/platform/native_sync_api.g.dart', + swiftOut: 'ios/Runner/Sync/Messages.g.swift', + swiftOptions: SwiftOptions(), + kotlinOut: + 'android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt', + kotlinOptions: KotlinOptions(package: 'app.alextran.immich.sync'), + dartOptions: DartOptions(), + dartPackageName: 'immich_mobile', + ), +) +class PlatformAsset { + final String id; + final String name; + // Follows AssetType enum from base_asset.model.dart + final int type; + // Seconds since epoch + final int? createdAt; + final int? updatedAt; + final int durationInSeconds; + + const PlatformAsset({ + required this.id, + required this.name, + required this.type, + this.createdAt, + this.updatedAt, + this.durationInSeconds = 0, + }); +} + +class PlatformAlbum { + final String id; + final String name; + // Seconds since epoch + final int? updatedAt; + final bool isCloud; + final int assetCount; + + const PlatformAlbum({ + required this.id, + required this.name, + this.updatedAt, + this.isCloud = false, + this.assetCount = 0, + }); +} + +class SyncDelta { + final bool hasChanges; + final List updates; + final List deletes; + // Asset -> Album mapping + final Map> assetAlbums; + + const SyncDelta({ + this.hasChanges = false, + this.updates = const [], + this.deletes = const [], + this.assetAlbums = const {}, + }); +} + +@HostApi() +abstract class NativeSyncApi { + bool shouldFullSync(); + + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + SyncDelta getMediaChanges(); + + void checkpointSync(); + + void clearSyncCheckpoint(); + + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + List getAssetIdsForAlbum(String albumId); + + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + List getAlbums(); + + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + int getAssetsCountSince(String albumId, int timestamp); + + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + List getAssetsForAlbum(String albumId, {int? updatedTimeCond}); +} diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 3df4e4e8a98..5c54a2c349d 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -5,31 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" + sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57 url: "https://pub.dev" source: hosted - version: "76.0.0" - _macros: - dependency: transitive - description: dart - source: sdk - version: "0.3.3" + version: "80.0.0" analyzer: - dependency: "direct overridden" + dependency: transitive description: name: analyzer - sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" + sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e" url: "https://pub.dev" source: hosted - version: "6.11.0" + version: "7.3.0" analyzer_plugin: - dependency: "direct overridden" + dependency: transitive description: name: analyzer_plugin - sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" + sha256: b3075265c5ab222f8b3188342dcb50b476286394a40323e85d1fa725035d40a4 url: "https://pub.dev" source: hosted - version: "0.11.3" + version: "0.13.0" ansicolor: dependency: transitive description: @@ -74,10 +69,10 @@ packages: dependency: "direct dev" description: name: auto_route_generator - sha256: c9086eb07271e51b44071ad5cff34e889f3156710b964a308c2ab590769e79e6 + sha256: c2e359d8932986d4d1bcad7a428143f81384ce10fef8d4aa5bc29e1f83766a46 url: "https://pub.dev" source: hosted - version: "9.0.0" + version: "9.3.1" background_downloader: dependency: "direct main" description: @@ -322,34 +317,42 @@ packages: dependency: "direct dev" description: name: custom_lint - sha256: "4500e88854e7581ee43586abeaf4443cb22375d6d289241a87b1aadf678d5545" + sha256: "409c485fd14f544af1da965d5a0d160ee57cd58b63eeaa7280a4f28cf5bda7f1" url: "https://pub.dev" source: hosted - version: "0.6.10" + version: "0.7.5" custom_lint_builder: dependency: transitive description: name: custom_lint_builder - sha256: "5a95eff100da256fbf086b329c17c8b49058c261cdf56d3a4157d3c31c511d78" + sha256: "107e0a43606138015777590ee8ce32f26ba7415c25b722ff0908a6f5d7a4c228" url: "https://pub.dev" source: hosted - version: "0.6.10" + version: "0.7.5" custom_lint_core: dependency: transitive description: name: custom_lint_core - sha256: "76a4046cc71d976222a078a8fd4a65e198b70545a8d690a75196dd14f08510f6" + sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be" url: "https://pub.dev" source: hosted - version: "0.6.10" + version: "0.7.5" + custom_lint_visitor: + dependency: transitive + description: + name: custom_lint_visitor + sha256: "36282d85714af494ee2d7da8c8913630aa6694da99f104fb2ed4afcf8fc857d8" + url: "https://pub.dev" + source: hosted + version: "1.0.0+7.3.0" dart_style: dependency: transitive description: name: dart_style - sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" + sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af" url: "https://pub.dev" source: hosted - version: "2.3.8" + version: "3.1.0" dartx: dependency: transitive description: @@ -723,10 +726,10 @@ packages: dependency: transitive description: name: freezed_annotation - sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "3.0.0" frontend_server_client: dependency: transitive description: @@ -971,10 +974,11 @@ packages: isar_generator: dependency: "direct dev" description: - name: isar_generator - sha256: "484e73d3b7e81dbd816852fe0b9497333118a9aeb646fd2d349a62cc8980ffe1" - url: "https://pub.isar-community.dev" - source: hosted + path: "packages/isar_generator" + ref: v3 + resolved-ref: ad574f60ed6f39d2995cd16fc7dc3de9a646ef30 + url: "https://github.com/immich-app/isar" + source: git version: "3.1.8" js: dependency: transitive @@ -1072,14 +1076,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - macros: - dependency: transitive - description: - name: macros - sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" - url: "https://pub.dev" - source: hosted - version: "0.1.3-main.0" maplibre_gl: dependency: "direct main" description: @@ -1121,7 +1117,7 @@ packages: source: hosted version: "0.11.1" meta: - dependency: "direct overridden" + dependency: transitive description: name: meta sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c @@ -1352,6 +1348,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + pigeon: + dependency: "direct dev" + description: + name: pigeon + sha256: a093af76026160bb5ff6eb98e3e678a301ffd1001ac0d90be558bc133a0c73f5 + url: "https://pub.dev" + source: hosted + version: "25.3.2" pinput: dependency: "direct main" description: @@ -1361,7 +1365,7 @@ packages: source: hosted version: "5.0.1" platform: - dependency: transitive + dependency: "direct main" description: name: platform sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" @@ -1444,10 +1448,10 @@ packages: dependency: transitive description: name: riverpod_analyzer_utils - sha256: "0dcb0af32d561f8fa000c6a6d95633c9fb08ea8a8df46e3f9daca59f11218167" + sha256: "03a17170088c63aab6c54c44456f5ab78876a1ddb6032ffde1662ddab4959611" url: "https://pub.dev" source: hosted - version: "0.5.6" + version: "0.5.10" riverpod_annotation: dependency: "direct main" description: @@ -1460,18 +1464,18 @@ packages: dependency: "direct dev" description: name: riverpod_generator - sha256: "851aedac7ad52693d12af3bf6d92b1626d516ed6b764eb61bf19e968b5e0b931" + sha256: "44a0992d54473eb199ede00e2260bd3c262a86560e3c6f6374503d86d0580e36" url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "2.6.5" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: "0684c21a9a4582c28c897d55c7b611fa59a351579061b43f8c92c005804e63a8" + sha256: "89a52b7334210dbff8605c3edf26cfe69b15062beed5cbfeff2c3812c33c9e35" url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "2.6.5" rxdart: dependency: transitive description: @@ -1633,10 +1637,10 @@ packages: dependency: transitive description: name: source_gen - sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "2.0.0" source_span: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 6dd81b7fc19..81249fdcfa0 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -32,6 +32,7 @@ dependencies: flutter_displaymode: ^0.6.0 flutter_hooks: ^0.21.2 flutter_local_notifications: ^17.2.1+2 + flutter_secure_storage: ^9.2.4 flutter_svg: ^2.0.17 flutter_udid: ^3.0.0 flutter_web_auth_2: ^5.0.0-alpha.0 @@ -41,6 +42,7 @@ dependencies: http: ^1.3.0 image_picker: ^1.1.2 intl: ^0.19.0 + local_auth: ^2.3.0 logging: ^1.3.0 maplibre_gl: ^0.21.0 network_info_plus: ^6.1.3 @@ -52,6 +54,8 @@ dependencies: permission_handler: ^11.4.0 photo_manager: ^3.6.4 photo_manager_image_provider: ^2.2.0 + pinput: ^5.0.1 + platform: ^3.1.6 punycode: ^1.0.0 riverpod_annotation: ^2.6.1 scrollable_positioned_list: ^0.3.8 @@ -64,9 +68,6 @@ dependencies: uuid: ^4.5.1 wakelock_plus: ^1.2.10 worker_manager: ^7.2.3 - local_auth: ^2.3.0 - pinput: ^5.0.1 - flutter_secure_storage: ^9.2.4 native_video_player: git: @@ -84,11 +85,6 @@ dependencies: drift: ^2.23.1 drift_flutter: ^0.2.4 -dependency_overrides: - analyzer: ^6.0.0 - meta: ^1.11.0 - analyzer_plugin: ^0.11.3 - dev_dependencies: flutter_test: sdk: flutter @@ -98,11 +94,13 @@ dev_dependencies: flutter_launcher_icons: ^0.14.3 flutter_native_splash: ^2.4.5 isar_generator: - version: *isar_version - hosted: https://pub.isar-community.dev/ + git: + url: https://github.com/immich-app/isar + ref: v3 + path: packages/isar_generator/ integration_test: sdk: flutter - custom_lint: ^0.6.4 + custom_lint: ^0.7.5 riverpod_lint: ^2.6.1 riverpod_generator: ^2.6.1 mocktail: ^1.0.4 @@ -112,6 +110,8 @@ dev_dependencies: file: ^7.0.1 # for MemoryFileSystem # Drift generator drift_dev: ^2.23.1 + # Type safe platform code + pigeon: ^25.3.1 flutter: uses-material-design: true From 0f42babb6bd7616fc51e995019d111a00521a30e Mon Sep 17 00:00:00 2001 From: Thien Dang <89862334+dvbthien@users.noreply.github.com> Date: Fri, 30 May 2025 03:06:08 +0700 Subject: [PATCH 12/50] fix: Update locked folder text and improve translations (#18622) * Update locked folder text and remove unused translations * uppercase Locked folder in Menu * convert some translates to icu and improve * add iOS debug info translations for background processes * fix lint --------- Co-authored-by: dvbthien --- i18n/en.json | 53 +++---------------- mobile/lib/pages/albums/albums.page.dart | 13 +++-- .../widgets/album/album_thumbnail_card.dart | 21 ++++---- .../album/album_thumbnail_listtile.dart | 20 ++++--- .../widgets/backup/ios_debug_info_tile.dart | 28 ++++++---- .../actions/set-visibility-action.svelte | 2 +- .../actions/set-visibility-action.svelte | 4 +- 7 files changed, 57 insertions(+), 84 deletions(-) diff --git a/i18n/en.json b/i18n/en.json index 603eb58b3db..014c8caa80a 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -26,7 +26,6 @@ "add_to_album": "Add to album", "add_to_album_bottom_sheet_added": "Added to {album}", "add_to_album_bottom_sheet_already_exists": "Already in {album}", - "add_to_locked_folder": "Add to locked folder", "add_to_shared_album": "Add to shared album", "add_url": "Add URL", "added_to_archive": "Added to archive", @@ -45,8 +44,6 @@ "backup_keep_last_amount": "Amount of previous dumps to keep", "backup_settings": "Database Dump Settings", "backup_settings_description": "Manage database dump settings. Note: These jobs are not monitored and you will not be notified of failure.", - "check_all": "Check All", - "cleanup": "Cleanup", "cleared_jobs": "Cleared jobs for: {job}", "config_set_by_file": "Config is currently set by a config file", "confirm_delete_library": "Are you sure you want to delete {library} library?", @@ -62,14 +59,12 @@ "disable_login": "Disable login", "duplicate_detection_job_description": "Run machine learning on assets to detect similar images. Relies on Smart Search", "exclusion_pattern_description": "Exclusion patterns lets you ignore files and folders when scanning your library. This is useful if you have folders that contain files you don't want to import, such as RAW files.", - "external_library_created_at": "External library (created on {date})", "external_library_management": "External Library Management", "face_detection": "Face detection", "face_detection_description": "Detect the faces in assets using machine learning. For videos, only the thumbnail is considered. \"Refresh\" (re-)processes all assets. \"Reset\" additionally clears all current face data. \"Missing\" queues assets that haven't been processed yet. Detected faces will be queued for Facial Recognition after Face Detection is complete, grouping them into existing or new people.", "facial_recognition_job_description": "Group detected faces into people. This step runs after Face Detection is complete. \"Reset\" (re-)clusters all faces. \"Missing\" queues faces that don't have a person assigned.", "failed_job_command": "Command {command} failed for job: {job}", "force_delete_user_warning": "WARNING: This will immediately remove the user and all assets. This cannot be undone and the files cannot be recovered.", - "forcing_refresh_library_files": "Forcing refresh of all library files", "image_format": "Format", "image_format_description": "WebP produces smaller files than JPEG, but is slower to encode.", "image_fullsize_description": "Full-size image with stripped metadata, used when zoomed in", @@ -210,8 +205,6 @@ "oauth_storage_quota_default_description": "Quota in GiB to be used when no claim is provided (Enter 0 for unlimited quota).", "oauth_timeout": "Request Timeout", "oauth_timeout_description": "Timeout for requests in milliseconds", - "offline_paths": "Offline Paths", - "offline_paths_description": "These results may be due to manual deletion of files that are not part of an external library.", "password_enable_description": "Login with email and password", "password_settings": "Password Login", "password_settings_description": "Manage password login settings", @@ -221,9 +214,6 @@ "refreshing_all_libraries": "Refreshing all libraries", "registration": "Admin Registration", "registration_description": "Since you are the first user on the system, you will be assigned as the Admin and are responsible for administrative tasks, and additional users will be created by you.", - "repair_all": "Repair All", - "repair_matched_items": "Matched {count, plural, one {# item} other {# items}}", - "repaired_items": "Repaired {count, plural, one {# item} other {# items}}", "require_password_change_on_login": "Require user to change password on first login", "reset_settings_to_default": "Reset settings to default", "reset_settings_to_recent_saved": "Reset settings to the recent saved settings", @@ -264,7 +254,6 @@ "template_email_invite_album": "Invite Album Template", "template_email_preview": "Preview", "template_email_settings": "Email Templates", - "template_email_settings_description": "Manage custom email notification templates", "template_email_update_album": "Update Album Template", "template_email_welcome": "Welcome email template", "template_settings": "Notification Templates", @@ -273,7 +262,6 @@ "theme_custom_css_settings_description": "Cascading Style Sheets allow the design of Immich to be customized.", "theme_settings": "Theme Settings", "theme_settings_description": "Manage customization of the Immich web interface", - "these_files_matched_by_checksum": "These files are matched by their checksums", "thumbnail_generation_job": "Generate Thumbnails", "thumbnail_generation_job_description": "Generate large, small and blurred thumbnails for each asset, as well as thumbnails for each person", "transcoding_acceleration_api": "Acceleration API", @@ -341,8 +329,6 @@ "trash_number_of_days_description": "Number of days to keep the assets in trash before permanently removing them", "trash_settings": "Trash Settings", "trash_settings_description": "Manage trash settings", - "untracked_files": "Untracked Files", - "untracked_files_description": "These files are not tracked by the application. They can be the results of failed moves, interrupted uploads, or left behind due to a bug", "user_cleanup_job": "User cleanup", "user_delete_delay": "{user}'s account and assets will be scheduled for permanent deletion in {delay, plural, one {# day} other {# days}}.", "user_delete_delay_settings": "Delete delay", @@ -401,10 +387,6 @@ "album_remove_user": "Remove user?", "album_remove_user_confirmation": "Are you sure you want to remove {user}?", "album_share_no_users": "Looks like you have shared this album with all users or you don't have any user to share with.", - "album_thumbnail_card_item": "1 item", - "album_thumbnail_card_items": "{count} items", - "album_thumbnail_card_shared": " · Shared", - "album_thumbnail_shared_by": "Shared by {user}", "album_updated": "Album updated", "album_updated_setting_description": "Receive an email notification when a shared album has new assets", "album_user_left": "Left {album}", @@ -576,21 +558,17 @@ "bulk_keep_duplicates_confirmation": "Are you sure you want to keep {count, plural, one {# duplicate asset} other {# duplicate assets}}? This will resolve all duplicate groups without deleting anything.", "bulk_trash_duplicates_confirmation": "Are you sure you want to bulk trash {count, plural, one {# duplicate asset} other {# duplicate assets}}? This will keep the largest asset of each group and trash all other duplicates.", "buy": "Purchase Immich", - "cache_settings_album_thumbnails": "Library page thumbnails ({count} assets)", "cache_settings_clear_cache_button": "Clear cache", "cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.", "cache_settings_duplicated_assets_clear_button": "CLEAR", "cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app", "cache_settings_duplicated_assets_title": "Duplicated Assets ({count})", - "cache_settings_image_cache_size": "Image cache size ({count} assets)", "cache_settings_statistics_album": "Library thumbnails", - "cache_settings_statistics_assets": "{count} assets ({size})", "cache_settings_statistics_full": "Full images", "cache_settings_statistics_shared": "Shared album thumbnails", "cache_settings_statistics_thumbnail": "Thumbnails", "cache_settings_statistics_title": "Cache usage", "cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application", - "cache_settings_thumbnail_size": "Thumbnail cache size ({count} assets)", "cache_settings_tile_subtitle": "Control the local storage behaviour", "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Caching Settings", @@ -622,7 +600,6 @@ "change_pin_code": "Change PIN code", "change_your_password": "Change your password", "changed_visibility_successfully": "Changed visibility successfully", - "check_all": "Check All", "check_corrupt_asset_backup": "Check for corrupt asset backups", "check_corrupt_asset_backup_button": "Perform check", "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", @@ -668,7 +645,6 @@ "contain": "Contain", "context": "Context", "continue": "Continue", - "control_bottom_app_bar_album_info_shared": "{count} items · Shared", "control_bottom_app_bar_create_new_album": "Create new album", "control_bottom_app_bar_delete_from_immich": "Delete from Immich", "control_bottom_app_bar_delete_from_local": "Delete from device", @@ -779,7 +755,6 @@ "download_enqueue": "Download enqueued", "download_error": "Download Error", "download_failed": "Download failed", - "download_filename": "file: {filename}", "download_finished": "Download finished", "download_include_embedded_motion_videos": "Embedded videos", "download_include_embedded_motion_videos_description": "Include videos embedded in motion photos as a separate file", @@ -855,7 +830,6 @@ "cant_get_number_of_comments": "Can't get number of comments", "cant_search_people": "Can't search people", "cant_search_places": "Can't search places", - "cleared_jobs": "Cleared jobs for: {job}", "error_adding_assets_to_album": "Error adding assets to album", "error_adding_users_to_album": "Error adding users to album", "error_deleting_shared_user": "Error deleting shared user", @@ -864,7 +838,6 @@ "error_removing_assets_from_album": "Error removing assets from album, check console for more details", "error_selecting_all_assets": "Error selecting all assets", "exclusion_pattern_already_exists": "This exclusion pattern already exists.", - "failed_job_command": "Command {command} failed for job: {job}", "failed_to_create_album": "Failed to create album", "failed_to_create_shared_link": "Failed to create shared link", "failed_to_edit_shared_link": "Failed to edit shared link", @@ -883,7 +856,6 @@ "paths_validation_failed": "{paths, plural, one {# path} other {# paths}} failed validation", "profile_picture_transparent_pixels": "Profile pictures cannot have transparent pixels. Please zoom in and/or move the image.", "quota_higher_than_disk_size": "You set a quota higher than the disk size", - "repair_unable_to_check_items": "Unable to check {count, select, one {item} other {items}}", "unable_to_add_album_users": "Unable to add users to album", "unable_to_add_assets_to_shared_link": "Unable to add assets to shared link", "unable_to_add_comment": "Unable to add comment", @@ -902,7 +874,6 @@ "unable_to_change_visibility": "Unable to change the visibility for {count, plural, one {# person} other {# people}}", "unable_to_complete_oauth_login": "Unable to complete OAuth login", "unable_to_connect": "Unable to connect", - "unable_to_connect_to_server": "Unable to connect to server", "unable_to_copy_to_clipboard": "Cannot copy to clipboard, make sure you are accessing the page through https", "unable_to_create_admin_account": "Unable to create admin account", "unable_to_create_api_key": "Unable to create a new API Key", @@ -926,14 +897,9 @@ "unable_to_hide_person": "Unable to hide person", "unable_to_link_motion_video": "Unable to link motion video", "unable_to_link_oauth_account": "Unable to link OAuth account", - "unable_to_load_album": "Unable to load album", - "unable_to_load_asset_activity": "Unable to load asset activity", - "unable_to_load_items": "Unable to load items", - "unable_to_load_liked_status": "Unable to load liked status", "unable_to_log_out_all_devices": "Unable to log out all devices", "unable_to_log_out_device": "Unable to log out device", "unable_to_login_with_oauth": "Unable to login with OAuth", - "unable_to_move_to_locked_folder": "Unable to move to locked folder", "unable_to_play_video": "Unable to play video", "unable_to_reassign_assets_existing_person": "Unable to reassign assets to {name, select, null {an existing person} other {{name}}}", "unable_to_reassign_assets_new_person": "Unable to reassign assets to a new person", @@ -941,11 +907,9 @@ "unable_to_remove_album_users": "Unable to remove users from album", "unable_to_remove_api_key": "Unable to remove API Key", "unable_to_remove_assets_from_shared_link": "Unable to remove assets from shared link", - "unable_to_remove_deleted_assets": "Unable to remove offline files", "unable_to_remove_library": "Unable to remove library", "unable_to_remove_partner": "Unable to remove partner", "unable_to_remove_reaction": "Unable to remove reaction", - "unable_to_repair_items": "Unable to repair items", "unable_to_reset_password": "Unable to reset password", "unable_to_reset_pin_code": "Unable to reset PIN code", "unable_to_resolve_duplicate": "Unable to resolve duplicate", @@ -1118,6 +1082,12 @@ "invalid_date_format": "Invalid date format", "invite_people": "Invite People", "invite_to_album": "Invite to album", + "ios_debug_info_fetch_ran_at": "Fetch ran {dateTime}", + "ios_debug_info_last_sync_at": "Last sync {dateTime}", + "ios_debug_info_no_processes_queued": "No background processes queued", + "ios_debug_info_no_sync_yet": "No background sync job has run yet", + "ios_debug_info_processes_queued": "{count, plural, one {{count} background process queued} other {{count} background processes queued}}", + "ios_debug_info_processing_ran_at": "Processing ran {dateTime}", "items_count": "{count, plural, one {# item} other {# items}}", "jobs": "Jobs", "keep": "Keep", @@ -1161,7 +1131,7 @@ "location_picker_longitude_error": "Enter a valid longitude", "location_picker_longitude_hint": "Enter your longitude here", "lock": "Lock", - "locked_folder": "Locked folder", + "locked_folder": "Locked Folder", "log_out": "Log out", "log_out_all_devices": "Log Out All Devices", "logged_out_all_devices": "Logged out all devices", @@ -1319,8 +1289,6 @@ "oauth": "OAuth", "official_immich_resources": "Official Immich Resources", "offline": "Offline", - "offline_paths": "Offline paths", - "offline_paths_description": "These results may be due to manual deletion of files that are not part of an external library.", "ok": "Ok", "oldest_first": "Oldest first", "on_this_device": "On this device", @@ -1883,8 +1851,6 @@ "unselect_all_duplicates": "Unselect all duplicates", "unstack": "Un-stack", "unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}", - "untracked_files": "Untracked files", - "untracked_files_decription": "These files are not tracked by the application. They can be the results of failed moves, interrupted uploads, or left behind due to a bug", "up_next": "Up next", "updated_at": "Updated", "updated_password": "Updated password", @@ -1927,11 +1893,6 @@ "version": "Version", "version_announcement_closing": "Your friend, Alex", "version_announcement_message": "Hi there! A new version of Immich is available. Please take some time to read the release notes to ensure your setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your Immich instance automatically.", - "version_announcement_overlay_release_notes": "release notes", - "version_announcement_overlay_text_1": "Hi friend, there is a new release of", - "version_announcement_overlay_text_2": "please take your time to visit the ", - "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", - "version_announcement_overlay_title": "New Server Version Available 🎉", "version_history": "Version History", "version_history_item": "Installed {version} on {date}", "video": "Video", diff --git a/mobile/lib/pages/albums/albums.page.dart b/mobile/lib/pages/albums/albums.page.dart index 8cc09a1ded3..9d8ebb7673b 100644 --- a/mobile/lib/pages/albums/albums.page.dart +++ b/mobile/lib/pages/albums/albums.page.dart @@ -14,6 +14,7 @@ import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/utils/translation.dart'; import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; @@ -229,13 +230,11 @@ class AlbumsPage extends HookConsumerWidget { ), subtitle: sorted[index].ownerId != null ? Text( - '${(sorted[index].assetCount == 1 ? 'album_thumbnail_card_item'.tr() : 'album_thumbnail_card_items'.tr( - namedArgs: { - 'count': sorted[index] - .assetCount - .toString(), - }, - ))} • ${sorted[index].ownerId != userId ? 'album_thumbnail_shared_by'.tr(namedArgs: {'user': sorted[index].ownerName!}) : 'owned'.tr()}', + '${t('items_count', { + 'count': sorted[index].assetCount, + })} • ${sorted[index].ownerId != userId ? t('shared_by_user', { + 'user': sorted[index].ownerName!, + }) : 'owned'.tr()}', overflow: TextOverflow.ellipsis, style: context.textTheme.bodyMedium?.copyWith( diff --git a/mobile/lib/widgets/album/album_thumbnail_card.dart b/mobile/lib/widgets/album/album_thumbnail_card.dart index 79944ef15fd..9f78b6066dc 100644 --- a/mobile/lib/widgets/album/album_thumbnail_card.dart +++ b/mobile/lib/widgets/album/album_thumbnail_card.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/utils/translation.dart'; import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; class AlbumThumbnailCard extends ConsumerWidget { @@ -61,28 +62,24 @@ class AlbumThumbnailCard extends ConsumerWidget { if (album.ownerId == ref.read(currentUserProvider)?.id) { owner = 'owned'.tr(); } else if (album.ownerName != null) { - owner = 'album_thumbnail_shared_by' - .tr(namedArgs: {'user': album.ownerName!}); + owner = t('shared_by_user', {'user': album.ownerName!}); } } - return RichText( - overflow: TextOverflow.fade, - text: TextSpan( - style: context.textTheme.bodyMedium?.copyWith( - color: context.colorScheme.onSurfaceSecondary, - ), + return Text.rich( + TextSpan( children: [ TextSpan( - text: album.assetCount == 1 - ? 'album_thumbnail_card_item'.tr() - : 'album_thumbnail_card_items' - .tr(namedArgs: {'count': '${album.assetCount}'}), + text: t('items_count', {'count': album.assetCount}), ), if (owner != null) const TextSpan(text: ' • '), if (owner != null) TextSpan(text: owner), ], + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, + ), ), + overflow: TextOverflow.fade, ); } diff --git a/mobile/lib/widgets/album/album_thumbnail_listtile.dart b/mobile/lib/widgets/album/album_thumbnail_listtile.dart index 17c2a6bd12e..11ef5d329b3 100644 --- a/mobile/lib/widgets/album/album_thumbnail_listtile.dart +++ b/mobile/lib/widgets/album/album_thumbnail_listtile.dart @@ -7,6 +7,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; +import 'package:immich_mobile/utils/translation.dart'; import 'package:openapi/api.dart'; class AlbumThumbnailListTile extends StatelessWidget { @@ -90,20 +91,25 @@ class AlbumThumbnailListTile extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Text( - album.assetCount == 1 - ? 'album_thumbnail_card_item' - : 'album_thumbnail_card_items', + t('items_count', {'count': album.assetCount}), style: const TextStyle( fontSize: 12, ), - ).tr(namedArgs: {'count': '${album.assetCount}'}), - if (album.shared) + ), + if (album.shared) ...[ const Text( - 'album_thumbnail_card_shared', + ' • ', style: TextStyle( fontSize: 12, ), - ).tr(), + ), + Text( + 'shared'.tr(), + style: const TextStyle( + fontSize: 12, + ), + ), + ], ], ), ], diff --git a/mobile/lib/widgets/backup/ios_debug_info_tile.dart b/mobile/lib/widgets/backup/ios_debug_info_tile.dart index 7919e72ea69..04be0c00dcd 100644 --- a/mobile/lib/widgets/backup/ios_debug_info_tile.dart +++ b/mobile/lib/widgets/backup/ios_debug_info_tile.dart @@ -1,8 +1,9 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart'; -import 'package:intl/intl.dart'; +import 'package:immich_mobile/utils/translation.dart'; /// This is a simple debug widget which should be removed later on when we are /// more confident about background sync @@ -19,26 +20,35 @@ class IosDebugInfoTile extends HookConsumerWidget { final processing = settings.timeOfLastProcessing; final processes = settings.numberOfBackgroundTasksQueued; - final processOrProcesses = processes == 1 ? 'process' : 'processes'; - final numberOrZero = processes == 0 ? 'No' : processes.toString(); - final title = '$numberOrZero background $processOrProcesses queued'; + final String title; + if (processes == 0) { + title = 'ios_debug_info_no_processes_queued'.tr(); + } else { + title = t('ios_debug_info_processes_queued', {'count': processes}); + } final df = DateFormat.yMd().add_jm(); final String subtitle; if (fetch == null && processing == null) { - subtitle = 'No background sync job has run yet'; + subtitle = 'ios_debug_info_no_sync_yet'.tr(); } else if (fetch != null && processing == null) { - subtitle = 'Fetch ran ${df.format(fetch)}'; + subtitle = + t('ios_debug_info_fetch_ran_at', {'dateTime': df.format(fetch)}); } else if (processing != null && fetch == null) { - subtitle = 'Processing ran ${df.format(processing)}'; + subtitle = t( + 'ios_debug_info_processing_ran_at', + {'dateTime': df.format(processing)}, + ); } else { final fetchOrProcessing = fetch!.isAfter(processing!) ? fetch : processing; - subtitle = 'Last sync ${df.format(fetchOrProcessing)}'; + subtitle = t( + 'ios_debug_info_last_sync_at', + {'dateTime': df.format(fetchOrProcessing)}, + ); } return ListTile( - key: ValueKey(title), title: Text( title, style: TextStyle( diff --git a/web/src/lib/components/asset-viewer/actions/set-visibility-action.svelte b/web/src/lib/components/asset-viewer/actions/set-visibility-action.svelte index dff470f456d..ab974275241 100644 --- a/web/src/lib/components/asset-viewer/actions/set-visibility-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/set-visibility-action.svelte @@ -56,6 +56,6 @@ toggleLockedVisibility()} - text={isLocked ? $t('move_off_locked_folder') : $t('add_to_locked_folder')} + text={isLocked ? $t('move_off_locked_folder') : $t('move_to_locked_folder')} icon={isLocked ? mdiLockOpenVariantOutline : mdiLockOutline} /> diff --git a/web/src/lib/components/photos-page/actions/set-visibility-action.svelte b/web/src/lib/components/photos-page/actions/set-visibility-action.svelte index 407a92fadcd..3c897d6b010 100644 --- a/web/src/lib/components/photos-page/actions/set-visibility-action.svelte +++ b/web/src/lib/components/photos-page/actions/set-visibility-action.svelte @@ -55,7 +55,7 @@ {#if menuItem} {:else} @@ -67,6 +67,6 @@ variant="ghost" onclick={setLockedVisibility} > - {unlock ? $t('move_off_locked_folder') : $t('add_to_locked_folder')} + {unlock ? $t('move_off_locked_folder') : $t('move_to_locked_folder')} {/if} From aebd68e24e997f1e4fe27d691c35dcd40800c22a Mon Sep 17 00:00:00 2001 From: Nicholas <30300649+NicholasFlamy@users.noreply.github.com> Date: Fri, 30 May 2025 00:50:09 -0400 Subject: [PATCH 13/50] fix: change `URL` to `Url` in the Obtainium apk links api endpoint (#18764) change `URL` to `Url` --- server/src/services/server.service.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts index f07b8ee92e0..e871536c9f4 100644 --- a/server/src/services/server.service.ts +++ b/server/src/services/server.service.ts @@ -50,12 +50,12 @@ export class ServerService extends BaseService { } getAndroidLinks(): ServerApkLinksDto { - const baseURL = `https://github.com/immich-app/immich/releases/download/v${serverVersion.toString()}`; + const baseUrl = `https://github.com/immich-app/immich/releases/download/v${serverVersion.toString()}`; return { - arm64v8a: `${baseURL}/app-arm64-v8a-release.apk`, - armeabiv7a: `${baseURL}/app-armeabi-v7a-release.apk`, - universal: `${baseURL}/app-release.apk`, - x86_64: `${baseURL}/app-x86_64-release.apk`, + arm64v8a: `${baseUrl}/app-arm64-v8a-release.apk`, + armeabiv7a: `${baseUrl}/app-armeabi-v7a-release.apk`, + universal: `${baseUrl}/app-release.apk`, + x86_64: `${baseUrl}/app-x86_64-release.apk`, }; } From b854a3dd47073890be8dfdbab4a1be2cda372dc1 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Fri, 30 May 2025 20:26:35 +0530 Subject: [PATCH 14/50] feat(server): add originalFileName to SyncAssetV1 (#18767) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex --- mobile/openapi/lib/model/sync_asset_v1.dart | 10 +++++++++- open-api/immich-openapi-specs.json | 4 ++++ server/src/database.ts | 1 + server/src/dtos/sync.dto.ts | 1 + server/src/queries/sync.repository.sql | 2 ++ server/test/medium/specs/sync/sync-asset.spec.ts | 3 +++ .../test/medium/specs/sync/sync-partner-asset.spec.ts | 3 +++ 7 files changed, 23 insertions(+), 1 deletion(-) diff --git a/mobile/openapi/lib/model/sync_asset_v1.dart b/mobile/openapi/lib/model/sync_asset_v1.dart index f5d59b6ae9d..a3aa7365adb 100644 --- a/mobile/openapi/lib/model/sync_asset_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_v1.dart @@ -20,6 +20,7 @@ class SyncAssetV1 { required this.id, required this.isFavorite, required this.localDateTime, + required this.originalFileName, required this.ownerId, required this.thumbhash, required this.type, @@ -40,6 +41,8 @@ class SyncAssetV1 { DateTime? localDateTime; + String originalFileName; + String ownerId; String? thumbhash; @@ -57,6 +60,7 @@ class SyncAssetV1 { other.id == id && other.isFavorite == isFavorite && other.localDateTime == localDateTime && + other.originalFileName == originalFileName && other.ownerId == ownerId && other.thumbhash == thumbhash && other.type == type && @@ -72,13 +76,14 @@ class SyncAssetV1 { (id.hashCode) + (isFavorite.hashCode) + (localDateTime == null ? 0 : localDateTime!.hashCode) + + (originalFileName.hashCode) + (ownerId.hashCode) + (thumbhash == null ? 0 : thumbhash!.hashCode) + (type.hashCode) + (visibility.hashCode); @override - String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, id=$id, isFavorite=$isFavorite, localDateTime=$localDateTime, ownerId=$ownerId, thumbhash=$thumbhash, type=$type, visibility=$visibility]'; + String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, id=$id, isFavorite=$isFavorite, localDateTime=$localDateTime, originalFileName=$originalFileName, ownerId=$ownerId, thumbhash=$thumbhash, type=$type, visibility=$visibility]'; Map toJson() { final json = {}; @@ -105,6 +110,7 @@ class SyncAssetV1 { } else { // json[r'localDateTime'] = null; } + json[r'originalFileName'] = this.originalFileName; json[r'ownerId'] = this.ownerId; if (this.thumbhash != null) { json[r'thumbhash'] = this.thumbhash; @@ -132,6 +138,7 @@ class SyncAssetV1 { id: mapValueOfType(json, r'id')!, isFavorite: mapValueOfType(json, r'isFavorite')!, localDateTime: mapDateTime(json, r'localDateTime', r''), + originalFileName: mapValueOfType(json, r'originalFileName')!, ownerId: mapValueOfType(json, r'ownerId')!, thumbhash: mapValueOfType(json, r'thumbhash'), type: SyncAssetV1TypeEnum.fromJson(json[r'type'])!, @@ -190,6 +197,7 @@ class SyncAssetV1 { 'id', 'isFavorite', 'localDateTime', + 'originalFileName', 'ownerId', 'thumbhash', 'type', diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 410840388f2..538a16e0ad2 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -13127,6 +13127,9 @@ "nullable": true, "type": "string" }, + "originalFileName": { + "type": "string" + }, "ownerId": { "type": "string" }, @@ -13161,6 +13164,7 @@ "id", "isFavorite", "localDateTime", + "originalFileName", "ownerId", "thumbhash", "type", diff --git a/server/src/database.ts b/server/src/database.ts index cfccd70b750..4e40e5c241e 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -341,6 +341,7 @@ export const columns = { syncAsset: [ 'id', 'ownerId', + 'originalFileName', 'thumbhash', 'checksum', 'fileCreatedAt', diff --git a/server/src/dtos/sync.dto.ts b/server/src/dtos/sync.dto.ts index 0043cfb40b4..3e50762c8a8 100644 --- a/server/src/dtos/sync.dto.ts +++ b/server/src/dtos/sync.dto.ts @@ -59,6 +59,7 @@ export class SyncPartnerDeleteV1 { export class SyncAssetV1 { id!: string; ownerId!: string; + originalFileName!: string; thumbhash!: string | null; checksum!: string; fileCreatedAt!: Date | null; diff --git a/server/src/queries/sync.repository.sql b/server/src/queries/sync.repository.sql index f797f5c0b52..f9565b6effa 100644 --- a/server/src/queries/sync.repository.sql +++ b/server/src/queries/sync.repository.sql @@ -76,6 +76,7 @@ order by select "id", "ownerId", + "originalFileName", "thumbhash", "checksum", "fileCreatedAt", @@ -98,6 +99,7 @@ order by select "id", "ownerId", + "originalFileName", "thumbhash", "checksum", "fileCreatedAt", diff --git a/server/test/medium/specs/sync/sync-asset.spec.ts b/server/test/medium/specs/sync/sync-asset.spec.ts index 3cf6d7d30dd..b46ccd97e1d 100644 --- a/server/test/medium/specs/sync/sync-asset.spec.ts +++ b/server/test/medium/specs/sync/sync-asset.spec.ts @@ -23,12 +23,14 @@ describe.concurrent(SyncEntityType.AssetV1, () => { it('should detect and sync the first asset', async () => { const { auth, sut, getRepository, testSync } = await setup(); + const originalFileName = 'firstAsset'; const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA='; const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA='; const date = new Date().toISOString(); const assetRepo = getRepository('asset'); const asset = mediumFactory.assetInsert({ + originalFileName, ownerId: auth.user.id, checksum: Buffer.from(checksum, 'base64'), thumbhash: Buffer.from(thumbhash, 'base64'), @@ -48,6 +50,7 @@ describe.concurrent(SyncEntityType.AssetV1, () => { ack: expect.any(String), data: { id: asset.id, + originalFileName, ownerId: asset.ownerId, thumbhash, checksum, diff --git a/server/test/medium/specs/sync/sync-partner-asset.spec.ts b/server/test/medium/specs/sync/sync-partner-asset.spec.ts index 70e31eca4ce..125047b1bf1 100644 --- a/server/test/medium/specs/sync/sync-partner-asset.spec.ts +++ b/server/test/medium/specs/sync/sync-partner-asset.spec.ts @@ -23,6 +23,7 @@ describe.concurrent(SyncRequestType.PartnerAssetsV1, () => { it('should detect and sync the first partner asset', async () => { const { auth, sut, getRepository, testSync } = await setup(); + const originalFileName = 'firstPartnerAsset'; const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA='; const thumbhash = '2225vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA='; const date = new Date().toISOString(); @@ -34,6 +35,7 @@ describe.concurrent(SyncRequestType.PartnerAssetsV1, () => { const assetRepo = getRepository('asset'); const asset = mediumFactory.assetInsert({ ownerId: user2.id, + originalFileName, checksum: Buffer.from(checksum, 'base64'), thumbhash: Buffer.from(thumbhash, 'base64'), fileCreatedAt: date, @@ -56,6 +58,7 @@ describe.concurrent(SyncRequestType.PartnerAssetsV1, () => { data: { id: asset.id, ownerId: asset.ownerId, + originalFileName, thumbhash, checksum, deletedAt: null, From 4b9a7b2ce0d7c1941e6a6561f3fba2aad5d83d1b Mon Sep 17 00:00:00 2001 From: Yaros Date: Fri, 30 May 2025 18:04:20 +0200 Subject: [PATCH 15/50] fix(mobile): android status bar overlays icon in map (#18780) --- mobile/lib/pages/search/map/map.page.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/mobile/lib/pages/search/map/map.page.dart b/mobile/lib/pages/search/map/map.page.dart index b80b96f94f1..022bf5da5f6 100644 --- a/mobile/lib/pages/search/map/map.page.dart +++ b/mobile/lib/pages/search/map/map.page.dart @@ -395,6 +395,7 @@ class _MapWithMarker extends StatelessWidget { children: [ style.widgetWhen( onData: (style) => MapLibreMap( + attributionButtonMargins: const Point(8, kToolbarHeight), initialCameraPosition: CameraPosition( target: initialLocation ?? const LatLng(0, 0), zoom: initialLocation != null ? 12 : 0, From cbf68b006e3972941b9f8aa0b0e2320beb72209d Mon Sep 17 00:00:00 2001 From: Brandon Wees Date: Fri, 30 May 2025 13:17:32 -0500 Subject: [PATCH 16/50] chore: add google cast feature switch to user admin pane (#18783) add gogole cast feature switch to user admin pane --- web/src/routes/admin/users/[id]/+page.svelte | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/src/routes/admin/users/[id]/+page.svelte b/web/src/routes/admin/users/[id]/+page.svelte index a33f2e9ec57..8264fc6f54f 100644 --- a/web/src/routes/admin/users/[id]/+page.svelte +++ b/web/src/routes/admin/users/[id]/+page.svelte @@ -316,6 +316,9 @@ + + + From 97503d11c584f4fa9ff4a9337818714a4a5fc5a2 Mon Sep 17 00:00:00 2001 From: Yaros Date: Fri, 30 May 2025 20:18:22 +0200 Subject: [PATCH 17/50] fix(web): datetime in storage template example (#18784) fix: datetime in storage template example --- .../settings/storage-template/supported-datetime-panel.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib/components/admin-page/settings/storage-template/supported-datetime-panel.svelte b/web/src/lib/components/admin-page/settings/storage-template/supported-datetime-panel.svelte index 9d8ff51cc08..8d8d6f0fa7b 100644 --- a/web/src/lib/components/admin-page/settings/storage-template/supported-datetime-panel.svelte +++ b/web/src/lib/components/admin-page/settings/storage-template/supported-datetime-panel.svelte @@ -23,7 +23,7 @@

{$t('admin.storage_template_date_time_description')}

-

{$t('admin.storage_template_date_time_sample', { values: { date: '2022-02-03T04:56:05.250' } })}

+

{$t('admin.storage_template_date_time_sample', { values: { date: '2022-09-04T20:03:05.250' } })}

From 9d04853b34c0fe10732b6707493666c4931617aa Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Fri, 30 May 2025 22:04:52 +0200 Subject: [PATCH 18/50] fix: oauth (#18725) --- server/src/repositories/oauth.repository.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/repositories/oauth.repository.ts b/server/src/repositories/oauth.repository.ts index ea9f0b1901a..357b52a77af 100644 --- a/server/src/repositories/oauth.repository.ts +++ b/server/src/repositories/oauth.repository.ts @@ -40,8 +40,8 @@ export class OAuthRepository { redirect_uri: redirectUrl, scope: config.scope, state, - code_challenge: codeChallenge, - code_challenge_method: 'S256', + code_challenge: client.serverMetadata().supportsPKCE() ? codeChallenge : '', + code_challenge_method: client.serverMetadata().supportsPKCE() ? 'S256' : '', }).toString(); return { url, state, codeVerifier }; } From f4e4e6628ea96fc521b90934c758db45645bcd5c Mon Sep 17 00:00:00 2001 From: Yaros Date: Fri, 30 May 2025 23:45:29 +0200 Subject: [PATCH 19/50] fix(mobile): center loading spinner in people page (#18781) fix: center loading spinner in people page --- mobile/lib/pages/library/people/people_collection.page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/lib/pages/library/people/people_collection.page.dart b/mobile/lib/pages/library/people/people_collection.page.dart index 27daf0a8879..6ec05485460 100644 --- a/mobile/lib/pages/library/people/people_collection.page.dart +++ b/mobile/lib/pages/library/people/people_collection.page.dart @@ -133,7 +133,7 @@ class PeopleCollectionPage extends HookConsumerWidget { ); }, error: (error, stack) => const Text("error"), - loading: () => const CircularProgressIndicator(), + loading: () => const Center(child: CircularProgressIndicator()), ), ); }, From e2defbc49ad798644fcee6eb4a699e56d7149019 Mon Sep 17 00:00:00 2001 From: Frank de Lange Date: Sat, 31 May 2025 00:12:53 +0200 Subject: [PATCH 20/50] feat: start oauth with `autoLaunch=1` (#18763) * Add automatic OpenID Connect login by using parameter `autoLaunch=1` By launching Immich with `/auth/login?autoLaunch=1` an OpenID Connect login attempt is directly initated on installations where OAuth Auto Launch is not enabled. The intended use for this parameter is to enable Immich to be launched from e.g. Nextcloud using the _External sites_ app and the _oids_ OpenID Connect provider app so as to enable the user to directly interact with Immich without the need to press the `Login with ...` button. * Add documentation for autolaunch by navigating to `/auth/login?autoLaunch=1` * Look ma, no braces! _This could be a single line_ And now it is, as is its predecessor. * Change formatting to satisfy _prettier_ * if (condition) return true -> return condition * More _prettier_ reformatting * Look ma, braces! --- docs/docs/administration/oauth.md | 1 + web/src/lib/utils.ts | 4 ++++ web/src/routes/auth/login/+page.svelte | 5 ++++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/docs/administration/oauth.md b/docs/docs/administration/oauth.md index 2dc69909446..b60b5dbb8bf 100644 --- a/docs/docs/administration/oauth.md +++ b/docs/docs/administration/oauth.md @@ -93,6 +93,7 @@ The `.well-known/openid-configuration` part of the url is optional and will be a ## Auto Launch When Auto Launch is enabled, the login page will automatically redirect the user to the OAuth authorization url, to login with OAuth. To access the login screen again, use the browser's back button, or navigate directly to `/auth/login?autoLaunch=0`. +Auto Launch can also be enabled on a per-request basis by navigating to `/auth/login?authLaunch=1`, this can be useful in situations where Immich is called from e.g. Nextcloud using the _External sites_ app and the _oidc_ app so as to enable users to directly interact with a logged-in instance of Immich. ## Mobile Redirect URI diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index 645c485cc5a..bfb8998781a 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -275,6 +275,10 @@ export const oauth = { } return false; }, + isAutoLaunchEnabled: (location: Location) => { + const value = 'autoLaunch=1'; + return location.search.includes(value); + }, authorize: async (location: Location) => { const $t = get(t); try { diff --git a/web/src/routes/auth/login/+page.svelte b/web/src/routes/auth/login/+page.svelte index 5cce88ae2cd..7937a55f80c 100644 --- a/web/src/routes/auth/login/+page.svelte +++ b/web/src/routes/auth/login/+page.svelte @@ -53,7 +53,10 @@ } try { - if ($featureFlags.oauthAutoLaunch && !oauth.isAutoLaunchDisabled(globalThis.location)) { + if ( + ($featureFlags.oauthAutoLaunch && !oauth.isAutoLaunchDisabled(globalThis.location)) || + oauth.isAutoLaunchEnabled(globalThis.location) + ) { await goto(`${AppRoute.AUTH_LOGIN}?autoLaunch=0`, { replaceState: true }); await oauth.authorize(globalThis.location); return; From 3a5fed99e18553e0185e4c4020002afca556755c Mon Sep 17 00:00:00 2001 From: Nicholas <30300649+NicholasFlamy@users.noreply.github.com> Date: Sat, 31 May 2025 00:27:55 -0400 Subject: [PATCH 21/50] fix(server): rename `android-links` api endpoint to `apk-links` (#18790) * remove auth from endpoint and change android to apk * add auth back to `apk-links` --- mobile/openapi/README.md | 2 +- mobile/openapi/lib/api/server_api.dart | 10 +++++----- open-api/immich-openapi-specs.json | 4 ++-- open-api/typescript-sdk/src/fetch-client.ts | 4 ++-- server/src/controllers/server.controller.ts | 6 +++--- server/src/services/server.service.ts | 2 +- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 22264857e06..73bbe7c1ff7 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -184,7 +184,7 @@ Class | Method | HTTP request | Description *SearchApi* | [**searchSmart**](doc//SearchApi.md#searchsmart) | **POST** /search/smart | *ServerApi* | [**deleteServerLicense**](doc//ServerApi.md#deleteserverlicense) | **DELETE** /server/license | *ServerApi* | [**getAboutInfo**](doc//ServerApi.md#getaboutinfo) | **GET** /server/about | -*ServerApi* | [**getAndroidLinks**](doc//ServerApi.md#getandroidlinks) | **GET** /server/android-links | +*ServerApi* | [**getApkLinks**](doc//ServerApi.md#getapklinks) | **GET** /server/apk-links | *ServerApi* | [**getServerConfig**](doc//ServerApi.md#getserverconfig) | **GET** /server/config | *ServerApi* | [**getServerFeatures**](doc//ServerApi.md#getserverfeatures) | **GET** /server/features | *ServerApi* | [**getServerLicense**](doc//ServerApi.md#getserverlicense) | **GET** /server/license | diff --git a/mobile/openapi/lib/api/server_api.dart b/mobile/openapi/lib/api/server_api.dart index 4220e674714..7abdabcd3e1 100644 --- a/mobile/openapi/lib/api/server_api.dart +++ b/mobile/openapi/lib/api/server_api.dart @@ -90,10 +90,10 @@ class ServerApi { return null; } - /// Performs an HTTP 'GET /server/android-links' operation and returns the [Response]. - Future getAndroidLinksWithHttpInfo() async { + /// Performs an HTTP 'GET /server/apk-links' operation and returns the [Response]. + Future getApkLinksWithHttpInfo() async { // ignore: prefer_const_declarations - final apiPath = r'/server/android-links'; + final apiPath = r'/server/apk-links'; // ignore: prefer_final_locals Object? postBody; @@ -116,8 +116,8 @@ class ServerApi { ); } - Future getAndroidLinks() async { - final response = await getAndroidLinksWithHttpInfo(); + Future getApkLinks() async { + final response = await getApkLinksWithHttpInfo(); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 538a16e0ad2..f2886f59c08 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -5275,9 +5275,9 @@ ] } }, - "/server/android-links": { + "/server/apk-links": { "get": { - "operationId": "getAndroidLinks", + "operationId": "getApkLinks", "parameters": [], "responses": { "200": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 2b0e2849d12..ac660808651 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -2874,11 +2874,11 @@ export function getAboutInfo(opts?: Oazapfts.RequestOpts) { ...opts })); } -export function getAndroidLinks(opts?: Oazapfts.RequestOpts) { +export function getApkLinks(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: ServerApkLinksDto; - }>("/server/android-links", { + }>("/server/apk-links", { ...opts })); } diff --git a/server/src/controllers/server.controller.ts b/server/src/controllers/server.controller.ts index 5bc78574c6b..3544fce2a0b 100644 --- a/server/src/controllers/server.controller.ts +++ b/server/src/controllers/server.controller.ts @@ -35,10 +35,10 @@ export class ServerController { return this.service.getAboutInfo(); } - @Get('android-links') + @Get('apk-links') @Authenticated() - getAndroidLinks(): ServerApkLinksDto { - return this.service.getAndroidLinks(); + getApkLinks(): ServerApkLinksDto { + return this.service.getApkLinks(); } @Get('storage') diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts index e871536c9f4..bada717f4a5 100644 --- a/server/src/services/server.service.ts +++ b/server/src/services/server.service.ts @@ -49,7 +49,7 @@ export class ServerService extends BaseService { }; } - getAndroidLinks(): ServerApkLinksDto { + getApkLinks(): ServerApkLinksDto { const baseUrl = `https://github.com/immich-app/immich/releases/download/v${serverVersion.toString()}`; return { arm64v8a: `${baseUrl}/app-arm64-v8a-release.apk`, From d00c872dc1fb6ff5d7bbea06284ed25a0b8d69b7 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Sat, 31 May 2025 14:37:43 +0200 Subject: [PATCH 22/50] fix: cursed knowledge date index (#18801) --- docs/src/pages/cursed-knowledge.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/pages/cursed-knowledge.tsx b/docs/src/pages/cursed-knowledge.tsx index 6a0981a5967..534d8e95d04 100644 --- a/docs/src/pages/cursed-knowledge.tsx +++ b/docs/src/pages/cursed-knowledge.tsx @@ -33,7 +33,7 @@ const items: Item[] = [ url: 'https://github.com/immich-app/immich/pull/17974', text: '#17974', }, - date: new Date(2025, 5, 5), + date: new Date(2025, 4, 5), }, { icon: mdiMicrosoftWindows, From 9c18fef9b2c4de8833ef67d2ba18fd98e921053e Mon Sep 17 00:00:00 2001 From: Arno <46051866+arnolicious@users.noreply.github.com> Date: Sat, 31 May 2025 15:30:08 +0200 Subject: [PATCH 23/50] chore: Refactor external library modals (#18655) --- .../forms/library-import-paths-form.svelte | 98 +++++++-------- .../LibraryImportPathModal.svelte} | 24 ++-- .../LibraryRenameModal.svelte} | 11 +- .../LibraryUserPickerModal.svelte} | 11 +- .../admin/library-management/+page.svelte | 114 ++++++++---------- 5 files changed, 114 insertions(+), 144 deletions(-) rename web/src/lib/{components/forms/library-import-path-form.svelte => modals/LibraryImportPathModal.svelte} (82%) rename web/src/lib/{components/forms/library-rename-form.svelte => modals/LibraryRenameModal.svelte} (73%) rename web/src/lib/{components/forms/library-user-picker-form.svelte => modals/LibraryUserPickerModal.svelte} (82%) diff --git a/web/src/lib/components/forms/library-import-paths-form.svelte b/web/src/lib/components/forms/library-import-paths-form.svelte index 5acaaf2a8c3..d664f3f6350 100644 --- a/web/src/lib/components/forms/library-import-paths-form.svelte +++ b/web/src/lib/components/forms/library-import-paths-form.svelte @@ -1,6 +1,8 @@ -{#if addImportPath} - { - addImportPath = false; - importPathToAdd = null; - }} - /> -{/if} - -{#if editImportPath != undefined} - (editImportPath = null)} - /> -{/if} -
- +
@@ -204,10 +193,7 @@ icon={mdiPencilOutline} title={$t('edit_import_path')} size="16" - onclick={() => { - editImportPath = listIndex; - editedImportPath = validatedPath.importPath; - }} + onclick={() => onEditImportPath(listIndex)} /> @@ -221,7 +207,7 @@ {/if} diff --git a/web/src/lib/components/forms/library-import-path-form.svelte b/web/src/lib/modals/LibraryImportPathModal.svelte similarity index 82% rename from web/src/lib/components/forms/library-import-path-form.svelte rename to web/src/lib/modals/LibraryImportPathModal.svelte index ee2a273708c..56a5449fef8 100644 --- a/web/src/lib/components/forms/library-import-path-form.svelte +++ b/web/src/lib/modals/LibraryImportPathModal.svelte @@ -11,9 +11,7 @@ cancelText?: string; submitText?: string; isEditing?: boolean; - onCancel: () => void; - onSubmit: (importPath: string | null) => void; - onDelete?: () => void; + onClose: (data?: { action: 'delete' } | { action: 'submit'; importPath: string | null }) => void; } let { @@ -23,9 +21,7 @@ cancelText = $t('cancel'), submitText = $t('save'), isEditing = false, - onCancel, - onSubmit, - onDelete, + onClose, }: Props = $props(); onMount(() => { @@ -40,12 +36,12 @@ const onsubmit = (event: Event) => { event.preventDefault(); if (canSubmit) { - onSubmit(importPath); + onClose({ action: 'submit', importPath }); } }; - +

{$t('admin.library_import_path_description')}

@@ -65,13 +61,15 @@
- + {#if isEditing} - + {/if} - +
diff --git a/web/src/lib/components/forms/library-rename-form.svelte b/web/src/lib/modals/LibraryRenameModal.svelte similarity index 73% rename from web/src/lib/components/forms/library-rename-form.svelte rename to web/src/lib/modals/LibraryRenameModal.svelte index 0c04c77da10..9ba8048316e 100644 --- a/web/src/lib/components/forms/library-rename-form.svelte +++ b/web/src/lib/modals/LibraryRenameModal.svelte @@ -6,21 +6,20 @@ interface Props { library: Partial; - onCancel: () => void; - onSubmit: (library: Partial) => void; + onClose: (library?: Partial) => void; } - let { library, onCancel, onSubmit }: Props = $props(); + let { library, onClose }: Props = $props(); let newName = $state(library.name); const onsubmit = (event: Event) => { event.preventDefault(); - onSubmit({ ...library, name: newName }); + onClose({ ...library, name: newName }); }; - + @@ -31,7 +30,7 @@
- +
diff --git a/web/src/lib/components/forms/library-user-picker-form.svelte b/web/src/lib/modals/LibraryUserPickerModal.svelte similarity index 82% rename from web/src/lib/components/forms/library-user-picker-form.svelte rename to web/src/lib/modals/LibraryUserPickerModal.svelte index 43b3eb69f16..52cde12921a 100644 --- a/web/src/lib/components/forms/library-user-picker-form.svelte +++ b/web/src/lib/modals/LibraryUserPickerModal.svelte @@ -8,11 +8,10 @@ import { t } from 'svelte-i18n'; interface Props { - onCancel: () => void; - onSubmit: (ownerId: string) => void; + onClose: (ownerId?: string) => void; } - let { onCancel, onSubmit }: Props = $props(); + let { onClose }: Props = $props(); let ownerId: string = $state($user.id); @@ -25,11 +24,11 @@ const onsubmit = (event: Event) => { event.preventDefault(); - onSubmit(ownerId); + onClose(ownerId); }; - +

{$t('admin.note_cannot_be_changed_later')}

@@ -40,7 +39,7 @@
- +
diff --git a/web/src/routes/admin/library-management/+page.svelte b/web/src/routes/admin/library-management/+page.svelte index 401a8900140..ed968a7df46 100644 --- a/web/src/routes/admin/library-management/+page.svelte +++ b/web/src/routes/admin/library-management/+page.svelte @@ -1,9 +1,6 @@ @@ -83,6 +92,7 @@ tempSlideshowLook = handleToggle(option, lookOptions) || tempSlideshowLook; }} /> + ('slideshow-show-progressbar', true); const slideshowDelay = persisted('slideshow-delay', 5, {}); const slideshowTransition = persisted('slideshow-transition', true); + const slideshowAutoplay = persisted('slideshow-autoplay', true, {}); return { restartProgress: { @@ -69,6 +70,7 @@ function createSlideshowStore() { slideshowDelay, showProgressBar, slideshowTransition, + slideshowAutoplay, }; } From a02e1f5e7c85d1ca1454bc2f35f99835a7c3854b Mon Sep 17 00:00:00 2001 From: Brandon Wees Date: Mon, 2 Jun 2025 09:47:23 -0500 Subject: [PATCH 39/50] chore(web): migrate CircleIconButton to @immich/ui IconButton (#18486) * remove import and referenced file * first pass at replacing all CircleIconButtons * fix linting issues * fix combobox formatting issues * fix button context menu coloring * remove circle icon button from search history box * use theme switcher from UI lib * dark mode force the asset viewer icons * fix forced dark mode icons * dark mode memory viewer icons * fix: back button in memory viewer --------- Co-authored-by: Alex --- web/src/lib/cast/cast-button.svelte | 39 ++----- .../admin-page/jobs/job-tile.svelte | 10 +- .../machine-learning-settings.svelte | 14 ++- .../components/album-page/album-card.svelte | 13 +-- .../components/album-page/album-map.svelte | 12 ++- .../album-page/album-options.svelte | 2 +- .../album-page/album-shared-link.svelte | 12 ++- .../components/album-page/album-viewer.svelte | 18 ++-- .../asset-viewer/actions/close-action.svelte | 11 +- .../asset-viewer/actions/delete-action.svelte | 10 +- .../actions/download-action.svelte | 10 +- .../actions/favorite-action.svelte | 10 +- .../actions/motion-photo-action.svelte | 9 +- .../asset-viewer/actions/share-action.svelte | 11 +- .../actions/show-detail-action.svelte | 11 +- .../asset-viewer/activity-viewer.svelte | 23 ++-- .../asset-viewer/asset-viewer-nav-bar.svelte | 44 ++++---- .../asset-viewer/detail-panel.svelte | 53 ++++++---- .../asset-viewer/download-panel.svelte | 11 +- .../editor/crop-tool/crop-tool.svelte | 28 ++++- .../asset-viewer/editor/editor-panel.svelte | 18 +++- .../asset-viewer/slideshow-bar.svelte | 8 +- .../asset-viewer/video-remote-viewer.svelte | 10 +- .../__test__/circle-icon-button.spec.ts | 29 ----- .../buttons/circle-icon-button.svelte | 100 ------------------ .../lib/components/elements/search-bar.svelte | 24 +++-- .../faces-page/assign-face-side-panel.svelte | 45 ++++++-- .../manage-people-visibility.svelte | 30 +++++- .../faces-page/merge-face-selector.svelte | 12 ++- .../components/faces-page/people-card.svelte | 5 +- .../components/faces-page/people-list.svelte | 16 +-- .../faces-page/person-side-panel.svelte | 37 ++++--- .../forms/library-import-paths-form.svelte | 10 +- .../forms/library-scan-settings-form.svelte | 9 +- .../lib/components/layouts/ErrorLayout.svelte | 7 +- .../memory-page/memory-viewer.svelte | 68 ++++++++---- .../photos-page/actions/archive-action.svelte | 13 ++- .../actions/create-shared-link.svelte | 11 +- .../photos-page/actions/delete-assets.svelte | 20 +++- .../actions/download-action.svelte | 11 +- .../actions/favorite-action.svelte | 15 ++- .../actions/link-live-photo-action.svelte | 14 ++- .../actions/remove-from-album.svelte | 11 +- .../actions/remove-from-shared-link.svelte | 11 +- .../actions/select-all-assets.svelte | 10 +- .../photos-page/actions/tag-action.svelte | 13 ++- .../individual-shared-viewer.svelte | 27 ++++- .../shared-components/combobox.svelte | 12 ++- .../context-menu/button-context-menu.svelte | 20 ++-- .../shared-components/control-app-bar.svelte | 15 ++- .../shared-components/modal-header.svelte | 14 ++- .../navigation-bar/account-info-panel.svelte | 12 +-- .../navigation-bar/navigation-bar.svelte | 4 +- .../notification/notification-card.svelte | 12 ++- .../search-bar/search-bar.svelte | 33 +++++- .../search-bar/search-history-box.svelte | 12 ++- .../side-bar/purchase-info.svelte | 12 ++- .../shared-components/theme-button.svelte | 26 ++--- .../shared-components/tree/breadcrumbs.svelte | 20 ++-- .../shared-components/upload-panel.svelte | 32 +++--- .../actions/shared-link-copy.svelte | 11 +- .../actions/shared-link-delete.svelte | 11 +- .../actions/shared-link-edit.svelte | 11 +- .../sharedlinks-page/shared-link-card.svelte | 5 +- .../user-settings-page/device-card.svelte | 12 ++- .../partner-settings.svelte | 12 ++- .../user-api-key-list.svelte | 17 +-- web/src/lib/modals/AlbumUsersModal.svelte | 2 +- .../modals/PersonMergeSuggestionModal.svelte | 10 +- .../[[assetId=id]]/+page.svelte | 73 +++++++++---- .../[[assetId=id]]/+page.svelte | 11 +- .../[[assetId=id]]/+page.svelte | 20 +++- .../routes/(user)/user-settings/+page.svelte | 10 +- .../[[assetId=id]]/+page.svelte | 11 +- .../admin/library-management/+page.svelte | 3 +- 75 files changed, 822 insertions(+), 556 deletions(-) delete mode 100644 web/src/lib/components/elements/buttons/__test__/circle-icon-button.spec.ts delete mode 100644 web/src/lib/components/elements/buttons/circle-icon-button.svelte diff --git a/web/src/lib/cast/cast-button.svelte b/web/src/lib/cast/cast-button.svelte index c6be1c11d72..392418daa5b 100644 --- a/web/src/lib/cast/cast-button.svelte +++ b/web/src/lib/cast/cast-button.svelte @@ -2,44 +2,23 @@ import { t } from 'svelte-i18n'; import { onMount } from 'svelte'; import { mdiCast, mdiCastConnected } from '@mdi/js'; - import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import { CastDestinationType, castManager } from '$lib/managers/cast-manager.svelte'; import { GCastDestination } from '$lib/utils/cast/gcast-destination.svelte'; import { IconButton } from '@immich/ui'; - interface Props { - whiteHover?: boolean; - navBar?: boolean; - } - - let { whiteHover, navBar }: Props = $props(); - onMount(async () => { await castManager.initialize(); }); - - const getButtonColor = () => { - return castManager.isCasting ? 'primary' : whiteHover ? undefined : 'opaque'; - }; {#if castManager.availableDestinations.length > 0 && castManager.availableDestinations[0].type === CastDestinationType.GCAST} - {#if navBar} - void GCastDestination.showCastDialog()} - aria-label={$t('cast')} - /> - {:else} - - {/if} + void GCastDestination.showCastDialog()} + aria-label={$t('cast')} + /> {/if} diff --git a/web/src/lib/components/admin-page/jobs/job-tile.svelte b/web/src/lib/components/admin-page/jobs/job-tile.svelte index c77ff60f22b..e85232deaab 100644 --- a/web/src/lib/components/admin-page/jobs/job-tile.svelte +++ b/web/src/lib/components/admin-page/jobs/job-tile.svelte @@ -1,6 +1,5 @@ - + {#if albumMapViewManager.isInMapView} diff --git a/web/src/lib/components/album-page/album-options.svelte b/web/src/lib/components/album-page/album-options.svelte index 9fbcf4e2ad2..3a20e106022 100644 --- a/web/src/lib/components/album-page/album-options.svelte +++ b/web/src/lib/components/album-page/album-options.svelte @@ -167,7 +167,7 @@ {$t('role_editor')} {/if} {#if user.id !== album.ownerId} - + {#if role === AlbumUserRole.Viewer} handleUpdateSharedUserRole(user, AlbumUserRole.Editor)} diff --git a/web/src/lib/components/album-page/album-shared-link.svelte b/web/src/lib/components/album-page/album-shared-link.svelte index b56aa11b6d2..e7d6503da3c 100644 --- a/web/src/lib/components/album-page/album-shared-link.svelte +++ b/web/src/lib/components/album-page/album-shared-link.svelte @@ -1,9 +1,8 @@ - onClick(!isPlaying)} /> diff --git a/web/src/lib/components/asset-viewer/actions/share-action.svelte b/web/src/lib/components/asset-viewer/actions/share-action.svelte index 7e2ffa1b943..5ab60fcb4c5 100644 --- a/web/src/lib/components/asset-viewer/actions/share-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/share-action.svelte @@ -1,10 +1,10 @@ - + diff --git a/web/src/lib/components/asset-viewer/actions/show-detail-action.svelte b/web/src/lib/components/asset-viewer/actions/show-detail-action.svelte index 5f6ac715db8..8ac087bca6e 100644 --- a/web/src/lib/components/asset-viewer/actions/show-detail-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/show-detail-action.svelte @@ -1,6 +1,6 @@ - - - - - - diff --git a/web/src/lib/components/elements/search-bar.svelte b/web/src/lib/components/elements/search-bar.svelte index c852be3b68b..24402857046 100644 --- a/web/src/lib/components/elements/search-bar.svelte +++ b/web/src/lib/components/elements/search-bar.svelte @@ -1,9 +1,9 @@ - + diff --git a/web/src/lib/components/photos-page/actions/delete-assets.svelte b/web/src/lib/components/photos-page/actions/delete-assets.svelte index 5cdcffb9371..b16e0465230 100644 --- a/web/src/lib/components/photos-page/actions/delete-assets.svelte +++ b/web/src/lib/components/photos-page/actions/delete-assets.svelte @@ -1,5 +1,4 @@ - + diff --git a/web/src/lib/components/photos-page/actions/select-all-assets.svelte b/web/src/lib/components/photos-page/actions/select-all-assets.svelte index 8fa73516093..f07bfd53ccc 100644 --- a/web/src/lib/components/photos-page/actions/select-all-assets.svelte +++ b/web/src/lib/components/photos-page/actions/select-all-assets.svelte @@ -1,9 +1,8 @@ {#if !themeManager.theme.system} - themeManager.toggleTheme()} - {padding} + themeManager.setTheme(theme == 'dark' ? Theme.DARK : Theme.LIGHT)} /> {/if} diff --git a/web/src/lib/components/shared-components/tree/breadcrumbs.svelte b/web/src/lib/components/shared-components/tree/breadcrumbs.svelte index a0d4d250f7c..135dda0acae 100644 --- a/web/src/lib/components/shared-components/tree/breadcrumbs.svelte +++ b/web/src/lib/components/shared-components/tree/breadcrumbs.svelte @@ -1,6 +1,6 @@ - (isOpen = true) }} /> + {#if isOwner && !authManager.key}
@@ -75,16 +68,10 @@ type="button" class="rounded-full bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-gray-700 dark:hover:text-gray-200 flex place-items-center place-content-center gap-1 px-2 py-1" title="Add tag" - onclick={handleAdd} + onclick={handleAddTag} > Add
{/if} - -{#if isOpen} - - handleTag(tagsIds)} onCancel={handleCancel} /> - -{/if} diff --git a/web/src/lib/components/photos-page/actions/tag-action.svelte b/web/src/lib/components/photos-page/actions/tag-action.svelte index 8915897a748..5bad8ca3279 100644 --- a/web/src/lib/components/photos-page/actions/tag-action.svelte +++ b/web/src/lib/components/photos-page/actions/tag-action.svelte @@ -1,10 +1,10 @@ - (isOpen = true) }} /> + {#if menuItem} - + {/if} {#if !menuItem} - {#if loading} - {}} - /> - {:else} - - {/if} -{/if} - -{#if isOpen} - handleTag(tagIds)} onCancel={handleCancel} /> + {/if} diff --git a/web/src/lib/components/forms/tag-asset-form.svelte b/web/src/lib/modals/AssetTagModal.svelte similarity index 83% rename from web/src/lib/components/forms/tag-asset-form.svelte rename to web/src/lib/modals/AssetTagModal.svelte index a6e0928ecee..2a61f0b9454 100644 --- a/web/src/lib/components/forms/tag-asset-form.svelte +++ b/web/src/lib/modals/AssetTagModal.svelte @@ -1,19 +1,20 @@ - +
@@ -95,7 +99,7 @@
- +
From e7d7886f446ef0ebc4a60f89c257fe3aa29cfd2d Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Mon, 2 Jun 2025 20:22:22 +0200 Subject: [PATCH 43/50] chore: move slideshow settings modal to modals folder (#18869) --- web/src/lib/components/asset-viewer/slideshow-bar.svelte | 4 ++-- .../SlideshowSettingsModal.svelte} | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) rename web/src/lib/{components/slideshow-settings.svelte => modals/SlideshowSettingsModal.svelte} (94%) diff --git a/web/src/lib/components/asset-viewer/slideshow-bar.svelte b/web/src/lib/components/asset-viewer/slideshow-bar.svelte index df35c6fb02f..135da12d264 100644 --- a/web/src/lib/components/asset-viewer/slideshow-bar.svelte +++ b/web/src/lib/components/asset-viewer/slideshow-bar.svelte @@ -1,9 +1,9 @@ diff --git a/web/src/lib/components/slideshow-settings.svelte b/web/src/lib/modals/SlideshowSettingsModal.svelte similarity index 94% rename from web/src/lib/components/slideshow-settings.svelte rename to web/src/lib/modals/SlideshowSettingsModal.svelte index d2e457377a5..d97b86fb128 100644 --- a/web/src/lib/components/slideshow-settings.svelte +++ b/web/src/lib/modals/SlideshowSettingsModal.svelte @@ -12,9 +12,9 @@ mdiShuffle, } from '@mdi/js'; import { t } from 'svelte-i18n'; + import type { RenderedOption } from '../components/elements/dropdown.svelte'; + import SettingDropdown from '../components/shared-components/settings/setting-dropdown.svelte'; import { SlideshowLook, SlideshowNavigation, slideshowStore } from '../stores/slideshow.store'; - import type { RenderedOption } from './elements/dropdown.svelte'; - import SettingDropdown from './shared-components/settings/setting-dropdown.svelte'; const { slideshowDelay, @@ -26,10 +26,10 @@ } = slideshowStore; interface Props { - onClose?: () => void; + onClose: () => void; } - let { onClose = () => {} }: Props = $props(); + let { onClose }: Props = $props(); // Temporary variables to hold the settings - marked as reactive with $state() but initialized with store values let tempSlideshowDelay = $state($slideshowDelay); From 74438f5bd8a7ded0d33ac2d0800445bcf8cf7444 Mon Sep 17 00:00:00 2001 From: Brandon Wees Date: Mon, 2 Jun 2025 16:09:13 -0500 Subject: [PATCH 44/50] feat(web): improved user onboarding (#18782) * wip * added user metadata key * wip * restructure onboarding system and add initial locale * update language card and fix translation updating * remove prints * new card formattings * fix cursed unmount effect * add OAuth route onboarding * remove required admin auth for onboarding * delete the hotwire button * update open-api files * delete import * fix failing oauth onboarding fields * fix e2e test * fix web e2e test * add onboarding to user registration e2e test * remove todo this was a holdover during dev and didn't get deleted * fix server small tests * use onDestroy to save settings rather than a bind:this * change to false for isOnboarded * fix other auth small test * provide type annotation in user factory metadata field * remove onboardingCompelted from UserDto * move translations to onboarding steps array and mark as derived so they update * break language selector out into its own component as per @danieldietzler suggestion * remove hello header on card * fix flixkering on server privacy card * label/id fixes * openapi --------- Co-authored-by: Alex Tran --- e2e/src/responses.ts | 1 + e2e/src/web/specs/auth.e2e-spec.ts | 11 +- i18n/en.json | 8 +- mobile/openapi/README.md | 5 + mobile/openapi/lib/api.dart | 2 + mobile/openapi/lib/api/users_api.dart | 121 +++++++++++++++ mobile/openapi/lib/api_client.dart | 4 + .../openapi/lib/model/login_response_dto.dart | 10 +- mobile/openapi/lib/model/onboarding_dto.dart | 99 ++++++++++++ .../lib/model/onboarding_response_dto.dart | 99 ++++++++++++ open-api/immich-openapi-specs.json | 121 +++++++++++++++ open-api/typescript-sdk/src/fetch-client.ts | 33 ++++ server/src/controllers/user.controller.ts | 19 +++ server/src/dtos/auth.dto.ts | 9 +- server/src/dtos/onboarding.dto.ts | 9 ++ server/src/enum.ts | 1 + server/src/services/auth.service.spec.ts | 2 + server/src/services/user.service.ts | 34 +++++ server/src/types.ts | 1 + server/test/small.factory.ts | 10 +- .../onboarding-page/onboarding-card.svelte | 52 ++++++- .../onboarding-page/onboarding-hello.svelte | 31 ++-- .../onboarding-language.svelte | 12 ++ .../onboarding-page/onboarding-privacy.svelte | 74 --------- .../onboarding-server-privacy.svelte | 35 +++++ .../onboarding-storage-template.svelte | 48 +----- .../onboarding-page/onboarding-theme.svelte | 36 +---- .../onboarding-user-privacy.svelte | 32 ++++ .../shared-components/combobox.svelte | 2 +- .../settings-language-selector.svelte | 58 +++++++ .../user-settings-page/app-settings.svelte | 36 +---- web/src/lib/models/onboarding-role.ts | 4 + web/src/lib/stores/server-config.store.ts | 17 ++- web/src/routes/auth/login/+page.svelte | 16 +- web/src/routes/auth/onboarding/+page.svelte | 142 ++++++++++++++---- web/src/routes/auth/onboarding/+page.ts | 2 +- 36 files changed, 961 insertions(+), 235 deletions(-) create mode 100644 mobile/openapi/lib/model/onboarding_dto.dart create mode 100644 mobile/openapi/lib/model/onboarding_response_dto.dart create mode 100644 server/src/dtos/onboarding.dto.ts create mode 100644 web/src/lib/components/onboarding-page/onboarding-language.svelte delete mode 100644 web/src/lib/components/onboarding-page/onboarding-privacy.svelte create mode 100644 web/src/lib/components/onboarding-page/onboarding-server-privacy.svelte create mode 100644 web/src/lib/components/onboarding-page/onboarding-user-privacy.svelte create mode 100644 web/src/lib/components/shared-components/settings/settings-language-selector.svelte create mode 100644 web/src/lib/models/onboarding-role.ts diff --git a/e2e/src/responses.ts b/e2e/src/responses.ts index 0148f2e1e93..bb6d17a2483 100644 --- a/e2e/src/responses.ts +++ b/e2e/src/responses.ts @@ -103,6 +103,7 @@ export const loginResponseDto = { accessToken: expect.any(String), name: 'Immich Admin', isAdmin: true, + isOnboarded: false, profileImagePath: '', shouldChangePassword: true, userEmail: 'admin@immich.cloud', diff --git a/e2e/src/web/specs/auth.e2e-spec.ts b/e2e/src/web/specs/auth.e2e-spec.ts index 74bee64e0a9..0fde9a6ec64 100644 --- a/e2e/src/web/specs/auth.e2e-spec.ts +++ b/e2e/src/web/specs/auth.e2e-spec.ts @@ -33,7 +33,9 @@ test.describe('Registration', () => { // onboarding await expect(page).toHaveURL('/auth/onboarding'); await page.getByRole('button', { name: 'Theme' }).click(); - await page.getByRole('button', { name: 'Privacy' }).click(); + await page.getByRole('button', { name: 'Language' }).click(); + await page.getByRole('button', { name: 'Server Privacy' }).click(); + await page.getByRole('button', { name: 'User Privacy' }).click(); await page.getByRole('button', { name: 'Storage Template' }).click(); await page.getByRole('button', { name: 'Done' }).click(); @@ -77,6 +79,13 @@ test.describe('Registration', () => { await page.getByLabel('Password').fill('new-password'); await page.getByRole('button', { name: 'Login' }).click(); + // onboarding + await expect(page).toHaveURL('/auth/onboarding'); + await page.getByRole('button', { name: 'Theme' }).click(); + await page.getByRole('button', { name: 'Language' }).click(); + await page.getByRole('button', { name: 'User Privacy' }).click(); + await page.getByRole('button', { name: 'Done' }).click(); + // success await expect(page).toHaveURL(/\/photos/); }); diff --git a/i18n/en.json b/i18n/en.json index 3e37b2da655..98ca467c51e 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1294,9 +1294,11 @@ "oldest_first": "Oldest first", "on_this_device": "On this device", "onboarding": "Onboarding", - "onboarding_privacy_description": "The following (optional) features rely on external services, and can be disabled at any time in the administration settings.", + "onboarding_locale_description": "Select your preferred language. You can change this later in your settings.", + "onboarding_privacy_description": "The following (optional) features rely on external services, and can be disabled at any time in settings.", + "onboarding_server_welcome_description": "Let's get your instance set up with some common settings.", "onboarding_theme_description": "Choose a color theme for your instance. You can change this later in your settings.", - "onboarding_welcome_description": "Let's get your instance set up with some common settings.", + "onboarding_user_welcome_description": "Let's get you started!", "onboarding_welcome_user": "Welcome, {user}", "online": "Online", "only_favorites": "Only favorites", @@ -1608,6 +1610,7 @@ "server_info_box_server_url": "Server URL", "server_offline": "Server Offline", "server_online": "Server Online", + "server_privacy": "Server Privacy", "server_stats": "Server Stats", "server_version": "Server Version", "set": "Set", @@ -1879,6 +1882,7 @@ "user_liked": "{user} liked {type, select, photo {this photo} video {this video} asset {this asset} other {it}}", "user_pin_code_settings": "PIN Code", "user_pin_code_settings_description": "Manage your PIN code", + "user_privacy": "User Privacy", "user_purchase_settings": "Purchase", "user_purchase_settings_description": "Manage your purchase", "user_role_set": "Set {user} as {role}", diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 73bbe7c1ff7..4ff55e5db8d 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -247,13 +247,16 @@ Class | Method | HTTP request | Description *UsersApi* | [**createProfileImage**](doc//UsersApi.md#createprofileimage) | **POST** /users/profile-image | *UsersApi* | [**deleteProfileImage**](doc//UsersApi.md#deleteprofileimage) | **DELETE** /users/profile-image | *UsersApi* | [**deleteUserLicense**](doc//UsersApi.md#deleteuserlicense) | **DELETE** /users/me/license | +*UsersApi* | [**deleteUserOnboarding**](doc//UsersApi.md#deleteuseronboarding) | **DELETE** /users/me/onboarding | *UsersApi* | [**getMyPreferences**](doc//UsersApi.md#getmypreferences) | **GET** /users/me/preferences | *UsersApi* | [**getMyUser**](doc//UsersApi.md#getmyuser) | **GET** /users/me | *UsersApi* | [**getProfileImage**](doc//UsersApi.md#getprofileimage) | **GET** /users/{id}/profile-image | *UsersApi* | [**getUser**](doc//UsersApi.md#getuser) | **GET** /users/{id} | *UsersApi* | [**getUserLicense**](doc//UsersApi.md#getuserlicense) | **GET** /users/me/license | +*UsersApi* | [**getUserOnboarding**](doc//UsersApi.md#getuseronboarding) | **GET** /users/me/onboarding | *UsersApi* | [**searchUsers**](doc//UsersApi.md#searchusers) | **GET** /users | *UsersApi* | [**setUserLicense**](doc//UsersApi.md#setuserlicense) | **PUT** /users/me/license | +*UsersApi* | [**setUserOnboarding**](doc//UsersApi.md#setuseronboarding) | **PUT** /users/me/onboarding | *UsersApi* | [**updateMyPreferences**](doc//UsersApi.md#updatemypreferences) | **PUT** /users/me/preferences | *UsersApi* | [**updateMyUser**](doc//UsersApi.md#updatemyuser) | **PUT** /users/me | *UsersAdminApi* | [**createUserAdmin**](doc//UsersAdminApi.md#createuseradmin) | **POST** /admin/users | @@ -385,6 +388,8 @@ Class | Method | HTTP request | Description - [OAuthConfigDto](doc//OAuthConfigDto.md) - [OAuthTokenEndpointAuthMethod](doc//OAuthTokenEndpointAuthMethod.md) - [OnThisDayDto](doc//OnThisDayDto.md) + - [OnboardingDto](doc//OnboardingDto.md) + - [OnboardingResponseDto](doc//OnboardingResponseDto.md) - [PartnerDirection](doc//PartnerDirection.md) - [PartnerResponseDto](doc//PartnerResponseDto.md) - [PeopleResponse](doc//PeopleResponse.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 846db953dcb..87d14248ebd 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -177,6 +177,8 @@ part 'model/o_auth_callback_dto.dart'; part 'model/o_auth_config_dto.dart'; part 'model/o_auth_token_endpoint_auth_method.dart'; part 'model/on_this_day_dto.dart'; +part 'model/onboarding_dto.dart'; +part 'model/onboarding_response_dto.dart'; part 'model/partner_direction.dart'; part 'model/partner_response_dto.dart'; part 'model/people_response.dart'; diff --git a/mobile/openapi/lib/api/users_api.dart b/mobile/openapi/lib/api/users_api.dart index a48ec54cfe5..cd31617e74c 100644 --- a/mobile/openapi/lib/api/users_api.dart +++ b/mobile/openapi/lib/api/users_api.dart @@ -139,6 +139,39 @@ class UsersApi { } } + /// Performs an HTTP 'DELETE /users/me/onboarding' operation and returns the [Response]. + Future deleteUserOnboardingWithHttpInfo() async { + // ignore: prefer_const_declarations + final apiPath = r'/users/me/onboarding'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'DELETE', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future deleteUserOnboarding() async { + final response = await deleteUserOnboardingWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + /// Performs an HTTP 'GET /users/me/preferences' operation and returns the [Response]. Future getMyPreferencesWithHttpInfo() async { // ignore: prefer_const_declarations @@ -358,6 +391,47 @@ class UsersApi { return null; } + /// Performs an HTTP 'GET /users/me/onboarding' operation and returns the [Response]. + Future getUserOnboardingWithHttpInfo() async { + // ignore: prefer_const_declarations + final apiPath = r'/users/me/onboarding'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future getUserOnboarding() async { + final response = await getUserOnboardingWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'OnboardingResponseDto',) as OnboardingResponseDto; + + } + return null; + } + /// Performs an HTTP 'GET /users' operation and returns the [Response]. Future searchUsersWithHttpInfo() async { // ignore: prefer_const_declarations @@ -449,6 +523,53 @@ class UsersApi { return null; } + /// Performs an HTTP 'PUT /users/me/onboarding' operation and returns the [Response]. + /// Parameters: + /// + /// * [OnboardingDto] onboardingDto (required): + Future setUserOnboardingWithHttpInfo(OnboardingDto onboardingDto,) async { + // ignore: prefer_const_declarations + final apiPath = r'/users/me/onboarding'; + + // ignore: prefer_final_locals + Object? postBody = onboardingDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + apiPath, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [OnboardingDto] onboardingDto (required): + Future setUserOnboarding(OnboardingDto onboardingDto,) async { + final response = await setUserOnboardingWithHttpInfo(onboardingDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'OnboardingResponseDto',) as OnboardingResponseDto; + + } + return null; + } + /// Performs an HTTP 'PUT /users/me/preferences' operation and returns the [Response]. /// Parameters: /// diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 2657cece1cc..46936fa88b4 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -410,6 +410,10 @@ class ApiClient { return OAuthTokenEndpointAuthMethodTypeTransformer().decode(value); case 'OnThisDayDto': return OnThisDayDto.fromJson(value); + case 'OnboardingDto': + return OnboardingDto.fromJson(value); + case 'OnboardingResponseDto': + return OnboardingResponseDto.fromJson(value); case 'PartnerDirection': return PartnerDirectionTypeTransformer().decode(value); case 'PartnerResponseDto': diff --git a/mobile/openapi/lib/model/login_response_dto.dart b/mobile/openapi/lib/model/login_response_dto.dart index dbc82d07ba1..82a4f9b3ed7 100644 --- a/mobile/openapi/lib/model/login_response_dto.dart +++ b/mobile/openapi/lib/model/login_response_dto.dart @@ -15,6 +15,7 @@ class LoginResponseDto { LoginResponseDto({ required this.accessToken, required this.isAdmin, + required this.isOnboarded, required this.name, required this.profileImagePath, required this.shouldChangePassword, @@ -26,6 +27,8 @@ class LoginResponseDto { bool isAdmin; + bool isOnboarded; + String name; String profileImagePath; @@ -40,6 +43,7 @@ class LoginResponseDto { bool operator ==(Object other) => identical(this, other) || other is LoginResponseDto && other.accessToken == accessToken && other.isAdmin == isAdmin && + other.isOnboarded == isOnboarded && other.name == name && other.profileImagePath == profileImagePath && other.shouldChangePassword == shouldChangePassword && @@ -51,6 +55,7 @@ class LoginResponseDto { // ignore: unnecessary_parenthesis (accessToken.hashCode) + (isAdmin.hashCode) + + (isOnboarded.hashCode) + (name.hashCode) + (profileImagePath.hashCode) + (shouldChangePassword.hashCode) + @@ -58,12 +63,13 @@ class LoginResponseDto { (userId.hashCode); @override - String toString() => 'LoginResponseDto[accessToken=$accessToken, isAdmin=$isAdmin, name=$name, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, userEmail=$userEmail, userId=$userId]'; + String toString() => 'LoginResponseDto[accessToken=$accessToken, isAdmin=$isAdmin, isOnboarded=$isOnboarded, name=$name, profileImagePath=$profileImagePath, shouldChangePassword=$shouldChangePassword, userEmail=$userEmail, userId=$userId]'; Map toJson() { final json = {}; json[r'accessToken'] = this.accessToken; json[r'isAdmin'] = this.isAdmin; + json[r'isOnboarded'] = this.isOnboarded; json[r'name'] = this.name; json[r'profileImagePath'] = this.profileImagePath; json[r'shouldChangePassword'] = this.shouldChangePassword; @@ -83,6 +89,7 @@ class LoginResponseDto { return LoginResponseDto( accessToken: mapValueOfType(json, r'accessToken')!, isAdmin: mapValueOfType(json, r'isAdmin')!, + isOnboarded: mapValueOfType(json, r'isOnboarded')!, name: mapValueOfType(json, r'name')!, profileImagePath: mapValueOfType(json, r'profileImagePath')!, shouldChangePassword: mapValueOfType(json, r'shouldChangePassword')!, @@ -137,6 +144,7 @@ class LoginResponseDto { static const requiredKeys = { 'accessToken', 'isAdmin', + 'isOnboarded', 'name', 'profileImagePath', 'shouldChangePassword', diff --git a/mobile/openapi/lib/model/onboarding_dto.dart b/mobile/openapi/lib/model/onboarding_dto.dart new file mode 100644 index 00000000000..670b6a5c681 --- /dev/null +++ b/mobile/openapi/lib/model/onboarding_dto.dart @@ -0,0 +1,99 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class OnboardingDto { + /// Returns a new [OnboardingDto] instance. + OnboardingDto({ + required this.isOnboarded, + }); + + bool isOnboarded; + + @override + bool operator ==(Object other) => identical(this, other) || other is OnboardingDto && + other.isOnboarded == isOnboarded; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (isOnboarded.hashCode); + + @override + String toString() => 'OnboardingDto[isOnboarded=$isOnboarded]'; + + Map toJson() { + final json = {}; + json[r'isOnboarded'] = this.isOnboarded; + return json; + } + + /// Returns a new [OnboardingDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static OnboardingDto? fromJson(dynamic value) { + upgradeDto(value, "OnboardingDto"); + if (value is Map) { + final json = value.cast(); + + return OnboardingDto( + isOnboarded: mapValueOfType(json, r'isOnboarded')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = OnboardingDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = OnboardingDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of OnboardingDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = OnboardingDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'isOnboarded', + }; +} + diff --git a/mobile/openapi/lib/model/onboarding_response_dto.dart b/mobile/openapi/lib/model/onboarding_response_dto.dart new file mode 100644 index 00000000000..033466e96b4 --- /dev/null +++ b/mobile/openapi/lib/model/onboarding_response_dto.dart @@ -0,0 +1,99 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class OnboardingResponseDto { + /// Returns a new [OnboardingResponseDto] instance. + OnboardingResponseDto({ + required this.isOnboarded, + }); + + bool isOnboarded; + + @override + bool operator ==(Object other) => identical(this, other) || other is OnboardingResponseDto && + other.isOnboarded == isOnboarded; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (isOnboarded.hashCode); + + @override + String toString() => 'OnboardingResponseDto[isOnboarded=$isOnboarded]'; + + Map toJson() { + final json = {}; + json[r'isOnboarded'] = this.isOnboarded; + return json; + } + + /// Returns a new [OnboardingResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static OnboardingResponseDto? fromJson(dynamic value) { + upgradeDto(value, "OnboardingResponseDto"); + if (value is Map) { + final json = value.cast(); + + return OnboardingResponseDto( + isOnboarded: mapValueOfType(json, r'isOnboarded')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = OnboardingResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = OnboardingResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of OnboardingResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = OnboardingResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'isOnboarded', + }; +} + diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index f2886f59c08..286fa47c660 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7922,6 +7922,101 @@ ] } }, + "/users/me/onboarding": { + "delete": { + "operationId": "deleteUserOnboarding", + "parameters": [], + "responses": { + "200": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Users" + ] + }, + "get": { + "operationId": "getUserOnboarding", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OnboardingResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Users" + ] + }, + "put": { + "operationId": "setUserOnboarding", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OnboardingDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OnboardingResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Users" + ] + } + }, "/users/me/preferences": { "get": { "operationId": "getMyPreferences", @@ -10404,6 +10499,9 @@ "isAdmin": { "type": "boolean" }, + "isOnboarded": { + "type": "boolean" + }, "name": { "type": "string" }, @@ -10423,6 +10521,7 @@ "required": [ "accessToken", "isAdmin", + "isOnboarded", "name", "profileImagePath", "shouldChangePassword", @@ -11067,6 +11166,28 @@ ], "type": "object" }, + "OnboardingDto": { + "properties": { + "isOnboarded": { + "type": "boolean" + } + }, + "required": [ + "isOnboarded" + ], + "type": "object" + }, + "OnboardingResponseDto": { + "properties": { + "isOnboarded": { + "type": "boolean" + } + }, + "required": [ + "isOnboarded" + ], + "type": "object" + }, "PartnerDirection": { "enum": [ "shared-by", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index ac660808651..e3e12dc56ec 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -512,6 +512,7 @@ export type LoginCredentialDto = { export type LoginResponseDto = { accessToken: string; isAdmin: boolean; + isOnboarded: boolean; name: string; profileImagePath: string; shouldChangePassword: boolean; @@ -1470,6 +1471,12 @@ export type UserUpdateMeDto = { name?: string; password?: string; }; +export type OnboardingResponseDto = { + isOnboarded: boolean; +}; +export type OnboardingDto = { + isOnboarded: boolean; +}; export type CreateProfileImageDto = { file: Blob; }; @@ -3582,6 +3589,32 @@ export function setUserLicense({ licenseKeyDto }: { body: licenseKeyDto }))); } +export function deleteUserOnboarding(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/users/me/onboarding", { + ...opts, + method: "DELETE" + })); +} +export function getUserOnboarding(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: OnboardingResponseDto; + }>("/users/me/onboarding", { + ...opts + })); +} +export function setUserOnboarding({ onboardingDto }: { + onboardingDto: OnboardingDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: OnboardingResponseDto; + }>("/users/me/onboarding", oazapfts.json({ + ...opts, + method: "PUT", + body: onboardingDto + }))); +} export function getMyPreferences(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; diff --git a/server/src/controllers/user.controller.ts b/server/src/controllers/user.controller.ts index f1bdf160d38..6c6eae15ff6 100644 --- a/server/src/controllers/user.controller.ts +++ b/server/src/controllers/user.controller.ts @@ -17,6 +17,7 @@ import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger'; import { NextFunction, Response } from 'express'; import { AuthDto } from 'src/dtos/auth.dto'; import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; +import { OnboardingDto, OnboardingResponseDto } from 'src/dtos/onboarding.dto'; import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto'; import { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto'; import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto } from 'src/dtos/user.dto'; @@ -87,6 +88,24 @@ export class UserController { await this.service.deleteLicense(auth); } + @Get('me/onboarding') + @Authenticated() + getUserOnboarding(@Auth() auth: AuthDto): Promise { + return this.service.getOnboarding(auth); + } + + @Put('me/onboarding') + @Authenticated() + async setUserOnboarding(@Auth() auth: AuthDto, @Body() Onboarding: OnboardingDto): Promise { + return this.service.setOnboarding(auth, Onboarding); + } + + @Delete('me/onboarding') + @Authenticated() + async deleteUserOnboarding(@Auth() auth: AuthDto): Promise { + await this.service.deleteOnboarding(auth); + } + @Get(':id') @Authenticated() getUser(@Param() { id }: UUIDParamDto): Promise { diff --git a/server/src/dtos/auth.dto.ts b/server/src/dtos/auth.dto.ts index 2f3ae5c14bc..e94818b2b5d 100644 --- a/server/src/dtos/auth.dto.ts +++ b/server/src/dtos/auth.dto.ts @@ -2,7 +2,8 @@ import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator'; import { AuthApiKey, AuthSession, AuthSharedLink, AuthUser, UserAdmin } from 'src/database'; -import { ImmichCookie } from 'src/enum'; +import { ImmichCookie, UserMetadataKey } from 'src/enum'; +import { UserMetadataItem } from 'src/types'; import { Optional, PinCode, toEmail } from 'src/validation'; export type CookieResponse = { @@ -39,9 +40,14 @@ export class LoginResponseDto { profileImagePath!: string; isAdmin!: boolean; shouldChangePassword!: boolean; + isOnboarded!: boolean; } export function mapLoginResponse(entity: UserAdmin, accessToken: string): LoginResponseDto { + const onboardingMetadata = entity.metadata.find( + (item): item is UserMetadataItem => item.key === UserMetadataKey.ONBOARDING, + )?.value; + return { accessToken, userId: entity.id, @@ -50,6 +56,7 @@ export function mapLoginResponse(entity: UserAdmin, accessToken: string): LoginR isAdmin: entity.isAdmin, profileImagePath: entity.profileImagePath, shouldChangePassword: entity.shouldChangePassword, + isOnboarded: onboardingMetadata?.isOnboarded ?? false, }; } diff --git a/server/src/dtos/onboarding.dto.ts b/server/src/dtos/onboarding.dto.ts new file mode 100644 index 00000000000..0028fca006e --- /dev/null +++ b/server/src/dtos/onboarding.dto.ts @@ -0,0 +1,9 @@ +import { IsBoolean, IsNotEmpty } from 'class-validator'; + +export class OnboardingDto { + @IsBoolean() + @IsNotEmpty() + isOnboarded!: boolean; +} + +export class OnboardingResponseDto extends OnboardingDto {} diff --git a/server/src/enum.ts b/server/src/enum.ts index b00b0133937..e7e40eb122a 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -211,6 +211,7 @@ export enum SystemMetadataKey { export enum UserMetadataKey { PREFERENCES = 'preferences', LICENSE = 'license', + ONBOARDING = 'onboarding', } export enum UserAvatarColor { diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index 4bc5f1ce0b0..a773f4a1cfc 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -28,6 +28,7 @@ const oauthResponse = ({ name, profileImagePath, isAdmin: false, + isOnboarded: false, shouldChangePassword: false, }); @@ -101,6 +102,7 @@ describe(AuthService.name, () => { name: user.name, profileImagePath: user.profileImagePath, isAdmin: user.isAdmin, + isOnboarded: false, shouldChangePassword: user.shouldChangePassword, }); diff --git a/server/src/services/user.service.ts b/server/src/services/user.service.ts index a0304d51ad6..78f49fd7ae0 100644 --- a/server/src/services/user.service.ts +++ b/server/src/services/user.service.ts @@ -6,6 +6,7 @@ import { StorageCore } from 'src/cores/storage.core'; import { OnJob } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; +import { OnboardingDto, OnboardingResponseDto } from 'src/dtos/onboarding.dto'; import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto'; import { CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto'; import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto'; @@ -179,6 +180,39 @@ export class UserService extends BaseService { return { ...license, activatedAt }; } + async getOnboarding(auth: AuthDto): Promise { + const metadata = await this.userRepository.getMetadata(auth.user.id); + + const onboardingData = metadata.find( + (item): item is UserMetadataItem => item.key === UserMetadataKey.ONBOARDING, + )?.value; + + if (!onboardingData) { + return { isOnboarded: false }; + } + + return { + isOnboarded: onboardingData.isOnboarded, + }; + } + + async deleteOnboarding({ user }: AuthDto): Promise { + await this.userRepository.deleteMetadata(user.id, UserMetadataKey.ONBOARDING); + } + + async setOnboarding(auth: AuthDto, onboarding: OnboardingDto): Promise { + await this.userRepository.upsertMetadata(auth.user.id, { + key: UserMetadataKey.ONBOARDING, + value: { + isOnboarded: onboarding.isOnboarded, + }, + }); + + return { + isOnboarded: onboarding.isOnboarded, + }; + } + @OnJob({ name: JobName.USER_SYNC_USAGE, queue: QueueName.BACKGROUND_TASK }) async handleUserSyncUsage(): Promise { await this.userRepository.syncUsage(); diff --git a/server/src/types.ts b/server/src/types.ts index 9d5ba46e120..2e613c124e1 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -510,4 +510,5 @@ export interface UserPreferences { export interface UserMetadata extends Record> { [UserMetadataKey.PREFERENCES]: DeepPartial; [UserMetadataKey.LICENSE]: { licenseKey: string; activationKey: string; activatedAt: string }; + [UserMetadataKey.ONBOARDING]: { isOnboarded: boolean }; } diff --git a/server/test/small.factory.ts b/server/test/small.factory.ts index 75e36c1da2e..b70f02bcf5d 100644 --- a/server/test/small.factory.ts +++ b/server/test/small.factory.ts @@ -15,8 +15,8 @@ import { } from 'src/database'; import { MapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { AssetStatus, AssetType, AssetVisibility, MemoryType, Permission, UserStatus } from 'src/enum'; -import { OnThisDayData } from 'src/types'; +import { AssetStatus, AssetType, AssetVisibility, MemoryType, Permission, UserMetadataKey, UserStatus } from 'src/enum'; +import { OnThisDayData, UserMetadataItem } from 'src/types'; export const newUuid = () => randomUUID() as string; export const newUuids = () => @@ -146,6 +146,12 @@ const userFactory = (user: Partial = {}) => ({ avatarColor: null, profileImagePath: '', profileChangedAt: newDate(), + metadata: [ + { + key: UserMetadataKey.ONBOARDING, + value: 'true', + }, + ] as UserMetadataItem[], ...user, }); diff --git a/web/src/lib/components/onboarding-page/onboarding-card.svelte b/web/src/lib/components/onboarding-page/onboarding-card.svelte index 54951dfa094..4a373fc310b 100644 --- a/web/src/lib/components/onboarding-page/onboarding-card.svelte +++ b/web/src/lib/components/onboarding-page/onboarding-card.svelte @@ -1,15 +1,32 @@
{/if} {@render children?.()} + +
+ {#if previousTitle} +
+ +
+ {/if} + +
+ +
+
diff --git a/web/src/lib/components/onboarding-page/onboarding-hello.svelte b/web/src/lib/components/onboarding-page/onboarding-hello.svelte index 70df619ae0b..f1b1516bbe5 100644 --- a/web/src/lib/components/onboarding-page/onboarding-hello.svelte +++ b/web/src/lib/components/onboarding-page/onboarding-hello.svelte @@ -1,28 +1,21 @@ - - -

+

+ +

{$t('onboarding_welcome_user', { values: { user: $user.name } })}

-

{$t('onboarding_welcome_description')}

- -
- -
- +

+ {userRole == OnboardingRole.SERVER + ? $t('onboarding_server_welcome_description') + : $t('onboarding_user_welcome_description')} +

+
diff --git a/web/src/lib/components/onboarding-page/onboarding-language.svelte b/web/src/lib/components/onboarding-page/onboarding-language.svelte new file mode 100644 index 00000000000..a37b026f130 --- /dev/null +++ b/web/src/lib/components/onboarding-page/onboarding-language.svelte @@ -0,0 +1,12 @@ + + +
+

+ {$t('onboarding_locale_description')} +

+ + +
diff --git a/web/src/lib/components/onboarding-page/onboarding-privacy.svelte b/web/src/lib/components/onboarding-page/onboarding-privacy.svelte deleted file mode 100644 index 12f4084fbc3..00000000000 --- a/web/src/lib/components/onboarding-page/onboarding-privacy.svelte +++ /dev/null @@ -1,74 +0,0 @@ - - - -

- {$t('onboarding_privacy_description')} -

- - {#if config && $user} - - {#if config} - - -
-
- -
-
- -
-
- {/if} -
- {/if} -
diff --git a/web/src/lib/components/onboarding-page/onboarding-server-privacy.svelte b/web/src/lib/components/onboarding-page/onboarding-server-privacy.svelte new file mode 100644 index 00000000000..a4af880fb4b --- /dev/null +++ b/web/src/lib/components/onboarding-page/onboarding-server-privacy.svelte @@ -0,0 +1,35 @@ + + +
+

+ {$t('onboarding_privacy_description')} +

+ + {#if $systemConfig} + + + {/if} +
diff --git a/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte b/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte index de2ce7e9801..baa45779e55 100644 --- a/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte +++ b/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte @@ -5,18 +5,7 @@ import { featureFlags } from '$lib/stores/server-config.store'; import { user } from '$lib/stores/user.store'; import { getConfig, type SystemConfigDto } from '@immich/sdk'; - import { Button } from '@immich/ui'; - import { mdiArrowLeft, mdiCheck, mdiHarddisk } from '@mdi/js'; import { onMount } from 'svelte'; - import { t } from 'svelte-i18n'; - import OnboardingCard from './onboarding-card.svelte'; - - interface Props { - onDone: () => void; - onPrevious: () => void; - } - - let { onDone, onPrevious }: Props = $props(); let config: SystemConfigDto | undefined = $state(); let adminSettingsComponent = $state>(); @@ -24,9 +13,13 @@ onMount(async () => { config = await getConfig(); }); + + export const save = async () => { + await adminSettingsComponent?.handleSave({ storageTemplate: config?.storageTemplate }); + }; - +

{#snippet children({ message })} @@ -48,36 +41,9 @@ onSave={(config) => adminSettingsComponent?.handleSave(config)} onReset={(options) => adminSettingsComponent?.handleReset(options)} duration={0} - > -

-
- -
-
- -
-
- + /> {/if} {/snippet} {/if} - +
diff --git a/web/src/lib/components/onboarding-page/onboarding-theme.svelte b/web/src/lib/components/onboarding-page/onboarding-theme.svelte index b128a9755c5..26e8fd9c7ac 100644 --- a/web/src/lib/components/onboarding-page/onboarding-theme.svelte +++ b/web/src/lib/components/onboarding-page/onboarding-theme.svelte @@ -3,27 +3,16 @@ import Icon from '$lib/components/elements/icon.svelte'; import { Theme } from '$lib/constants'; import { themeManager } from '$lib/managers/theme-manager.svelte'; - import { Button } from '@immich/ui'; - import { mdiArrowRight, mdiThemeLightDark } from '@mdi/js'; import { t } from 'svelte-i18n'; - import OnboardingCard from './onboarding-card.svelte'; - - interface Props { - onDone: () => void; - } - - let { onDone }: Props = $props(); - -
-

{$t('onboarding_theme_description')}

-
+
+

{$t('onboarding_theme_description')}

-
+
- -
-
- -
-
- +
diff --git a/web/src/lib/components/onboarding-page/onboarding-user-privacy.svelte b/web/src/lib/components/onboarding-page/onboarding-user-privacy.svelte new file mode 100644 index 00000000000..d65ade1b184 --- /dev/null +++ b/web/src/lib/components/onboarding-page/onboarding-user-privacy.svelte @@ -0,0 +1,32 @@ + + +
+

+ {$t('onboarding_privacy_description')} +

+ + +
diff --git a/web/src/lib/components/shared-components/combobox.svelte b/web/src/lib/components/shared-components/combobox.svelte index 91282ae9bca..8d5800e9a8a 100644 --- a/web/src/lib/components/shared-components/combobox.svelte +++ b/web/src/lib/components/shared-components/combobox.svelte @@ -348,7 +348,7 @@
    + import { invalidateAll } from '$app/navigation'; + import Combobox from '$lib/components/shared-components/combobox.svelte'; + import { defaultLang, langs } from '$lib/constants'; + import { lang } from '$lib/stores/preferences.store'; + import { getClosestAvailableLocale, langCodes } from '$lib/utils/i18n'; + import { locale as i18nLocale, t } from 'svelte-i18n'; + + interface Props { + showSettingDescription?: boolean; + } + + let { showSettingDescription = false }: Props = $props(); + + const langOptions = langs + .map((lang) => ({ label: lang.name, value: lang.code })) + .sort((a, b) => { + if (b.label.startsWith('Development')) { + return -1; + } + return a.label.localeCompare(b.label); + }); + + const defaultLangOption = { label: defaultLang.name, value: defaultLang.code }; + + const handleLanguageChange = async (newLang: string | undefined) => { + if (newLang) { + $lang = newLang; + await i18nLocale.set(newLang); + await invalidateAll(); + } + }; + + let closestLanguage = $derived(getClosestAvailableLocale([$lang], langCodes)); + + +
    + {#if showSettingDescription} +
    +
    + +
    + +

    {$t('language_setting_description')}

    +
    + {/if} + + value === closestLanguage) || defaultLangOption} + placeholder={$t('language')} + onSelect={(event) => handleLanguageChange(event?.value)} + options={langOptions} + /> +
    diff --git a/web/src/lib/components/user-settings-page/app-settings.svelte b/web/src/lib/components/user-settings-page/app-settings.svelte index f1d8e147877..adb37d5d939 100644 --- a/web/src/lib/components/user-settings-page/app-settings.svelte +++ b/web/src/lib/components/user-settings-page/app-settings.svelte @@ -1,22 +1,20 @@
    @@ -103,14 +82,7 @@
- value === closestLanguage) || defaultLangOption} - options={langOptions} - title={$t('language')} - subtitle={$t('language_setting_description')} - onSelect={(combobox) => handleLanguageChange(combobox?.value)} - /> +
diff --git a/web/src/lib/models/onboarding-role.ts b/web/src/lib/models/onboarding-role.ts new file mode 100644 index 00000000000..4efc307932b --- /dev/null +++ b/web/src/lib/models/onboarding-role.ts @@ -0,0 +1,4 @@ +export enum OnboardingRole { + SERVER = 'server', + USER = 'user', +} diff --git a/web/src/lib/stores/server-config.store.ts b/web/src/lib/stores/server-config.store.ts index 254db719467..ce2d8c2842a 100644 --- a/web/src/lib/stores/server-config.store.ts +++ b/web/src/lib/stores/server-config.store.ts @@ -1,4 +1,11 @@ -import { getServerConfig, getServerFeatures, type ServerConfigDto, type ServerFeaturesDto } from '@immich/sdk'; +import { + getConfig, + getServerConfig, + getServerFeatures, + type ServerConfigDto, + type ServerFeaturesDto, + type SystemConfigDto, +} from '@immich/sdk'; import { writable } from 'svelte/store'; export type FeatureFlags = ServerFeaturesDto & { loaded: boolean }; @@ -37,9 +44,17 @@ export const serverConfig = writable({ publicUsers: true, }); +export type SystemConfig = SystemConfigDto & { loaded: boolean }; +export const systemConfig = writable(); + export const retrieveServerConfig = async () => { const [flags, config] = await Promise.all([getServerFeatures(), getServerConfig()]); featureFlags.update(() => ({ ...flags, loaded: true })); serverConfig.update(() => ({ ...config, loaded: true })); }; + +export const retrieveSystemConfig = async () => { + const config = await getConfig(); + systemConfig.update(() => ({ ...config, loaded: true })); +}; diff --git a/web/src/routes/auth/login/+page.svelte b/web/src/routes/auth/login/+page.svelte index 7937a55f80c..fca888006ad 100644 --- a/web/src/routes/auth/login/+page.svelte +++ b/web/src/routes/auth/login/+page.svelte @@ -26,7 +26,6 @@ let oauthLoading = $state(true); const onSuccess = async (user: LoginResponseDto) => { - console.log(data.continueUrl); await goto(data.continueUrl, { invalidateAll: true }); eventManager.emit('auth.login', user); }; @@ -43,6 +42,12 @@ if (oauth.isCallback(globalThis.location)) { try { const user = await oauth.login(globalThis.location); + + if (!user.isOnboarded) { + await onOnboarding(); + return; + } + await onSuccess(user); return; } catch (error) { @@ -79,10 +84,19 @@ return; } + // change the user password before we onboard them if (!user.isAdmin && user.shouldChangePassword) { await onFirstLogin(); return; } + + // We want to onboard after the first login since their password will change + // and handleLogin will be called again (relogin). We then do onboarding on that next call. + if (!user.isOnboarded) { + await onOnboarding(); + return; + } + await onSuccess(user); return; } catch (error) { diff --git a/web/src/routes/auth/onboarding/+page.svelte b/web/src/routes/auth/onboarding/+page.svelte index 091681002ec..2978e4fd2a1 100644 --- a/web/src/routes/auth/onboarding/+page.svelte +++ b/web/src/routes/auth/onboarding/+page.svelte @@ -1,17 +1,21 @@
@@ -61,11 +140,20 @@
- + + +
diff --git a/web/src/routes/auth/onboarding/+page.ts b/web/src/routes/auth/onboarding/+page.ts index 86c19c10a80..66cb3de2c10 100644 --- a/web/src/routes/auth/onboarding/+page.ts +++ b/web/src/routes/auth/onboarding/+page.ts @@ -3,7 +3,7 @@ import { getFormatter } from '$lib/utils/i18n'; import type { PageLoad } from './$types'; export const load = (async ({ url }) => { - await authenticate(url, { admin: true }); + await authenticate(url); const $t = await getFormatter(); From 393e8d50b29fc79a30fa18236939d853910addad Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 11:39:55 +0000 Subject: [PATCH 45/50] fix(deps): update typescript-projects (#18889) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- cli/package-lock.json | 146 +++--- docs/package-lock.json | 996 +++++++++++---------------------------- docs/package.json | 6 +- e2e/package-lock.json | 146 +++--- server/package-lock.json | 218 +++++---- web/package-lock.json | 348 +++++++------- 6 files changed, 755 insertions(+), 1105 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index 5373f3cdd15..9464f866630 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1370,17 +1370,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", - "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.0.tgz", + "integrity": "sha512-CACyQuqSHt7ma3Ns601xykeBK/rDeZa3w6IS6UtMQbixO5DWy+8TilKkviGDH6jtWCo8FGRKEK5cLLkPvEammQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/type-utils": "8.32.1", - "@typescript-eslint/utils": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/type-utils": "8.33.0", + "@typescript-eslint/utils": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -1394,7 +1394,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "@typescript-eslint/parser": "^8.33.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } @@ -1410,16 +1410,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", - "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.0.tgz", + "integrity": "sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/typescript-estree": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "debug": "^4.3.4" }, "engines": { @@ -1434,15 +1434,16 @@ "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", - "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.0.tgz", + "integrity": "sha512-d1hz0u9l6N+u/gcrk6s6gYdl7/+pp8yHheRTqP6X5hVDKALEaTn8WfGiit7G511yueBEL3OpOEpD+3/MBdoN+A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1" + "@typescript-eslint/tsconfig-utils": "^8.33.0", + "@typescript-eslint/types": "^8.33.0", + "debug": "^4.3.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1452,15 +1453,50 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", - "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.0.tgz", + "integrity": "sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/utils": "8.32.1", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.0.tgz", + "integrity": "sha512-sTkETlbqhEoiFmGr1gsdq5HyVbSOF0145SYDJ/EQmXHtKViCaGvnyLqWFFHtEXoS0J1yU8Wyou2UGmgW88fEug==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.33.0.tgz", + "integrity": "sha512-lScnHNCBqL1QayuSrWeqAL5GmqNdVUQAAMTaCwdYEdWfIrSrOGzyLGRCHXcCixa5NK6i5l0AfSO2oBSjCjf4XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.33.0", + "@typescript-eslint/utils": "8.33.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1477,9 +1513,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", - "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.0.tgz", + "integrity": "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg==", "dev": true, "license": "MIT", "engines": { @@ -1491,14 +1527,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", - "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.0.tgz", + "integrity": "sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/project-service": "8.33.0", + "@typescript-eslint/tsconfig-utils": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1544,16 +1582,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", - "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.33.0.tgz", + "integrity": "sha512-lPFuQaLA9aSNa7D5u2EpRiqdAUhzShwGg/nhpBlc4GR6kcTABttCuyjFs8BcEZ8VWrjCBof/bePhP3Q3fS+Yrw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1" + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/typescript-estree": "8.33.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1568,13 +1606,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", - "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.0.tgz", + "integrity": "sha512-7RW7CMYoskiz5OOGAWjJFxgb7c5UNjTG292gYhWeOAcFmYCtVCSqjqSBj5zMhxbXo2JOW95YYrUWJfU0zrpaGQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/types": "8.33.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -2756,9 +2794,9 @@ } }, "node_modules/globals": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz", - "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", + "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", "dev": true, "license": "MIT", "engines": { @@ -4200,15 +4238,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz", - "integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.33.0.tgz", + "integrity": "sha512-5YmNhF24ylCsvdNW2oJwMzTbaeO4bg90KeGtMjUw0AGtHksgEPLRTUil+coHwCfiu4QjVJFnjp94DmU6zV7DhQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.32.1", - "@typescript-eslint/parser": "8.32.1", - "@typescript-eslint/utils": "8.32.1" + "@typescript-eslint/eslint-plugin": "8.33.0", + "@typescript-eslint/parser": "8.33.0", + "@typescript-eslint/utils": "8.33.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/docs/package-lock.json b/docs/package-lock.json index 602232da074..ce1ba4cd9d1 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -8,8 +8,8 @@ "name": "documentation", "version": "0.0.0", "dependencies": { - "@docusaurus/core": "~3.7.0", - "@docusaurus/preset-classic": "~3.7.0", + "@docusaurus/core": "~3.8.0", + "@docusaurus/preset-classic": "~3.8.0", "@mdi/js": "^7.3.67", "@mdi/react": "^1.6.1", "@mdx-js/react": "^3.0.0", @@ -27,7 +27,7 @@ "url": "^0.11.0" }, "devDependencies": { - "@docusaurus/module-type-aliases": "~3.7.0", + "@docusaurus/module-type-aliases": "~3.8.0", "@docusaurus/tsconfig": "^3.7.0", "@docusaurus/types": "^3.7.0", "prettier": "^3.2.4", @@ -3128,9 +3128,9 @@ } }, "node_modules/@docusaurus/babel": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/babel/-/babel-3.7.0.tgz", - "integrity": "sha512-0H5uoJLm14S/oKV3Keihxvh8RV+vrid+6Gv+2qhuzbqHanawga8tYnsdpjEyt36ucJjqlby2/Md2ObWjA02UXQ==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/babel/-/babel-3.8.0.tgz", + "integrity": "sha512-9EJwSgS6TgB8IzGk1L8XddJLhZod8fXT4ULYMx6SKqyCBqCFpVCEjR/hNXXhnmtVM2irDuzYoVLGWv7srG/VOA==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.9", @@ -3143,8 +3143,8 @@ "@babel/runtime": "^7.25.9", "@babel/runtime-corejs3": "^7.25.9", "@babel/traverse": "^7.25.9", - "@docusaurus/logger": "3.7.0", - "@docusaurus/utils": "3.7.0", + "@docusaurus/logger": "3.8.0", + "@docusaurus/utils": "3.8.0", "babel-plugin-dynamic-import-node": "^2.3.3", "fs-extra": "^11.1.1", "tslib": "^2.6.0" @@ -3154,17 +3154,17 @@ } }, "node_modules/@docusaurus/bundler": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/bundler/-/bundler-3.7.0.tgz", - "integrity": "sha512-CUUT9VlSGukrCU5ctZucykvgCISivct+cby28wJwCC/fkQFgAHRp/GKv2tx38ZmXb7nacrKzFTcp++f9txUYGg==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/bundler/-/bundler-3.8.0.tgz", + "integrity": "sha512-Rq4Z/MSeAHjVzBLirLeMcjLIAQy92pF1OI+2rmt18fSlMARfTGLWRE8Vb+ljQPTOSfJxwDYSzsK6i7XloD2rNA==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.9", - "@docusaurus/babel": "3.7.0", - "@docusaurus/cssnano-preset": "3.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", + "@docusaurus/babel": "3.8.0", + "@docusaurus/cssnano-preset": "3.8.0", + "@docusaurus/logger": "3.8.0", + "@docusaurus/types": "3.8.0", + "@docusaurus/utils": "3.8.0", "babel-loader": "^9.2.1", "clean-css": "^5.3.2", "copy-webpack-plugin": "^11.0.0", @@ -3178,7 +3178,6 @@ "postcss": "^8.4.26", "postcss-loader": "^7.3.3", "postcss-preset-env": "^10.1.0", - "react-dev-utils": "^12.0.1", "terser-webpack-plugin": "^5.3.9", "tslib": "^2.6.0", "url-loader": "^4.1.1", @@ -3198,18 +3197,18 @@ } }, "node_modules/@docusaurus/core": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.7.0.tgz", - "integrity": "sha512-b0fUmaL+JbzDIQaamzpAFpTviiaU4cX3Qz8cuo14+HGBCwa0evEK0UYCBFY3n4cLzL8Op1BueeroUD2LYAIHbQ==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.8.0.tgz", + "integrity": "sha512-c7u6zFELmSGPEP9WSubhVDjgnpiHgDqMh1qVdCB7rTflh4Jx0msTYmMiO91Ez0KtHj4sIsDsASnjwfJ2IZp3Vw==", "license": "MIT", "dependencies": { - "@docusaurus/babel": "3.7.0", - "@docusaurus/bundler": "3.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/mdx-loader": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/babel": "3.8.0", + "@docusaurus/bundler": "3.8.0", + "@docusaurus/logger": "3.8.0", + "@docusaurus/mdx-loader": "3.8.0", + "@docusaurus/utils": "3.8.0", + "@docusaurus/utils-common": "3.8.0", + "@docusaurus/utils-validation": "3.8.0", "boxen": "^6.2.1", "chalk": "^4.1.2", "chokidar": "^3.5.3", @@ -3217,19 +3216,19 @@ "combine-promises": "^1.1.0", "commander": "^5.1.0", "core-js": "^3.31.1", - "del": "^6.1.1", "detect-port": "^1.5.1", "escape-html": "^1.0.3", "eta": "^2.2.0", "eval": "^0.1.8", + "execa": "5.1.1", "fs-extra": "^11.1.1", "html-tags": "^3.3.1", "html-webpack-plugin": "^5.6.0", "leven": "^3.1.0", "lodash": "^4.17.21", + "open": "^8.4.0", "p-map": "^4.0.0", "prompts": "^2.4.2", - "react-dev-utils": "^12.0.1", "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", "react-loadable": "npm:@docusaurus/react-loadable@6.0.0", "react-loadable-ssr-addon-v5-slorber": "^1.0.1", @@ -3238,7 +3237,7 @@ "react-router-dom": "^5.3.4", "semver": "^7.5.4", "serve-handler": "^6.1.6", - "shelljs": "^0.8.5", + "tinypool": "^1.0.2", "tslib": "^2.6.0", "update-notifier": "^6.0.2", "webpack": "^5.95.0", @@ -3259,9 +3258,9 @@ } }, "node_modules/@docusaurus/cssnano-preset": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.7.0.tgz", - "integrity": "sha512-X9GYgruZBSOozg4w4dzv9uOz8oK/EpPVQXkp0MM6Tsgp/nRIU9hJzJ0Pxg1aRa3xCeEQTOimZHcocQFlLwYajQ==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.8.0.tgz", + "integrity": "sha512-UJ4hAS2T0R4WNy+phwVff2Q0L5+RXW9cwlH6AEphHR5qw3m/yacfWcSK7ort2pMMbDn8uGrD38BTm4oLkuuNoQ==", "license": "MIT", "dependencies": { "cssnano-preset-advanced": "^6.1.2", @@ -3274,9 +3273,9 @@ } }, "node_modules/@docusaurus/logger": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.7.0.tgz", - "integrity": "sha512-z7g62X7bYxCYmeNNuO9jmzxLQG95q9QxINCwpboVcNff3SJiHJbGrarxxOVMVmAh1MsrSfxWkVGv4P41ktnFsA==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.8.0.tgz", + "integrity": "sha512-7eEMaFIam5Q+v8XwGqF/n0ZoCld4hV4eCCgQkfcN9Mq5inoZa6PHHW9Wu6lmgzoK5Kx3keEeABcO2SxwraoPDQ==", "license": "MIT", "dependencies": { "chalk": "^4.1.2", @@ -3287,21 +3286,21 @@ } }, "node_modules/@docusaurus/mdx-loader": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.7.0.tgz", - "integrity": "sha512-OFBG6oMjZzc78/U3WNPSHs2W9ZJ723ewAcvVJaqS0VgyeUfmzUV8f1sv+iUHA0DtwiR5T5FjOxj6nzEE8LY6VA==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.8.0.tgz", + "integrity": "sha512-mDPSzssRnpjSdCGuv7z2EIAnPS1MHuZGTaRLwPn4oQwszu4afjWZ/60sfKjTnjBjI8Vl4OgJl2vMmfmiNDX4Ng==", "license": "MIT", "dependencies": { - "@docusaurus/logger": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/logger": "3.8.0", + "@docusaurus/utils": "3.8.0", + "@docusaurus/utils-validation": "3.8.0", "@mdx-js/mdx": "^3.0.0", "@slorber/remark-comment": "^1.0.0", "escape-html": "^1.0.3", "estree-util-value-to-estree": "^3.0.1", "file-loader": "^6.2.0", "fs-extra": "^11.1.1", - "image-size": "^1.0.2", + "image-size": "^2.0.2", "mdast-util-mdx": "^3.0.0", "mdast-util-to-string": "^4.0.0", "rehype-raw": "^7.0.0", @@ -3326,17 +3325,17 @@ } }, "node_modules/@docusaurus/module-type-aliases": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.7.0.tgz", - "integrity": "sha512-g7WdPqDNaqA60CmBrr0cORTrsOit77hbsTj7xE2l71YhBn79sxdm7WMK7wfhcaafkbpIh7jv5ef5TOpf1Xv9Lg==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.8.0.tgz", + "integrity": "sha512-/uMb4Ipt5J/QnD13MpnoC/A4EYAe6DKNWqTWLlGrqsPJwJv73vSwkA25xnYunwfqWk0FlUQfGv/Swdh5eCCg7g==", "license": "MIT", "dependencies": { - "@docusaurus/types": "3.7.0", + "@docusaurus/types": "3.8.0", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", "@types/react-router-dom": "*", - "react-helmet-async": "npm:@slorber/react-helmet-async@*", + "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", "react-loadable": "npm:@docusaurus/react-loadable@6.0.0" }, "peerDependencies": { @@ -3345,24 +3344,24 @@ } }, "node_modules/@docusaurus/plugin-content-blog": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.7.0.tgz", - "integrity": "sha512-EFLgEz6tGHYWdPU0rK8tSscZwx+AsyuBW/r+tNig2kbccHYGUJmZtYN38GjAa3Fda4NU+6wqUO5kTXQSRBQD3g==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.8.0.tgz", + "integrity": "sha512-0SlOTd9R55WEr1GgIXu+hhTT0hzARYx3zIScA5IzpdekZQesI/hKEa5LPHBd415fLkWMjdD59TaW/3qQKpJ0Lg==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/mdx-loader": "3.7.0", - "@docusaurus/theme-common": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/core": "3.8.0", + "@docusaurus/logger": "3.8.0", + "@docusaurus/mdx-loader": "3.8.0", + "@docusaurus/theme-common": "3.8.0", + "@docusaurus/types": "3.8.0", + "@docusaurus/utils": "3.8.0", + "@docusaurus/utils-common": "3.8.0", + "@docusaurus/utils-validation": "3.8.0", "cheerio": "1.0.0-rc.12", "feed": "^4.2.2", "fs-extra": "^11.1.1", "lodash": "^4.17.21", - "reading-time": "^1.5.0", + "schema-dts": "^1.1.2", "srcset": "^4.0.0", "tslib": "^2.6.0", "unist-util-visit": "^5.0.0", @@ -3379,25 +3378,26 @@ } }, "node_modules/@docusaurus/plugin-content-docs": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.7.0.tgz", - "integrity": "sha512-GXg5V7kC9FZE4FkUZA8oo/NrlRb06UwuICzI6tcbzj0+TVgjq/mpUXXzSgKzMS82YByi4dY2Q808njcBCyy6tQ==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.8.0.tgz", + "integrity": "sha512-fRDMFLbUN6eVRXcjP8s3Y7HpAt9pzPYh1F/7KKXOCxvJhjjCtbon4VJW0WndEPInVz4t8QUXn5QZkU2tGVCE2g==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/mdx-loader": "3.7.0", - "@docusaurus/module-type-aliases": "3.7.0", - "@docusaurus/theme-common": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/core": "3.8.0", + "@docusaurus/logger": "3.8.0", + "@docusaurus/mdx-loader": "3.8.0", + "@docusaurus/module-type-aliases": "3.8.0", + "@docusaurus/theme-common": "3.8.0", + "@docusaurus/types": "3.8.0", + "@docusaurus/utils": "3.8.0", + "@docusaurus/utils-common": "3.8.0", + "@docusaurus/utils-validation": "3.8.0", "@types/react-router-config": "^5.0.7", "combine-promises": "^1.1.0", "fs-extra": "^11.1.1", "js-yaml": "^4.1.0", "lodash": "^4.17.21", + "schema-dts": "^1.1.2", "tslib": "^2.6.0", "utility-types": "^3.10.0", "webpack": "^5.88.1" @@ -3411,16 +3411,16 @@ } }, "node_modules/@docusaurus/plugin-content-pages": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.7.0.tgz", - "integrity": "sha512-YJSU3tjIJf032/Aeao8SZjFOrXJbz/FACMveSMjLyMH4itQyZ2XgUIzt4y+1ISvvk5zrW4DABVT2awTCqBkx0Q==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.8.0.tgz", + "integrity": "sha512-39EDx2y1GA0Pxfion5tQZLNJxL4gq6susd1xzetVBjVIQtwpCdyloOfQBAgX0FylqQxfJrYqL0DIUuq7rd7uBw==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/mdx-loader": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/core": "3.8.0", + "@docusaurus/mdx-loader": "3.8.0", + "@docusaurus/types": "3.8.0", + "@docusaurus/utils": "3.8.0", + "@docusaurus/utils-validation": "3.8.0", "fs-extra": "^11.1.1", "tslib": "^2.6.0", "webpack": "^5.88.1" @@ -3433,17 +3433,32 @@ "react-dom": "^18.0.0 || ^19.0.0" } }, - "node_modules/@docusaurus/plugin-debug": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.7.0.tgz", - "integrity": "sha512-Qgg+IjG/z4svtbCNyTocjIwvNTNEwgRjSXXSJkKVG0oWoH0eX/HAPiu+TS1HBwRPQV+tTYPWLrUypYFepfujZA==", + "node_modules/@docusaurus/plugin-css-cascade-layers": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-css-cascade-layers/-/plugin-css-cascade-layers-3.8.0.tgz", + "integrity": "sha512-/VBTNymPIxQB8oA3ZQ4GFFRYdH4ZxDRRBECxyjRyv486mfUPXfcdk+im4S5mKWa6EK2JzBz95IH/Wu0qQgJ5yQ==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", + "@docusaurus/core": "3.8.0", + "@docusaurus/types": "3.8.0", + "@docusaurus/utils-validation": "3.8.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=18.0" + } + }, + "node_modules/@docusaurus/plugin-debug": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.8.0.tgz", + "integrity": "sha512-teonJvJsDB9o2OnG6ifbhblg/PXzZvpUKHFgD8dOL1UJ58u0lk8o0ZOkvaYEBa9nDgqzoWrRk9w+e3qaG2mOhQ==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.8.0", + "@docusaurus/types": "3.8.0", + "@docusaurus/utils": "3.8.0", "fs-extra": "^11.1.1", - "react-json-view-lite": "^1.2.0", + "react-json-view-lite": "^2.3.0", "tslib": "^2.6.0" }, "engines": { @@ -3455,14 +3470,14 @@ } }, "node_modules/@docusaurus/plugin-google-analytics": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.7.0.tgz", - "integrity": "sha512-otIqiRV/jka6Snjf+AqB360XCeSv7lQC+DKYW+EUZf6XbuE8utz5PeUQ8VuOcD8Bk5zvT1MC4JKcd5zPfDuMWA==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.8.0.tgz", + "integrity": "sha512-aKKa7Q8+3xRSRESipNvlFgNp3FNPELKhuo48Cg/svQbGNwidSHbZT03JqbW4cBaQnyyVchO1ttk+kJ5VC9Gx0w==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/core": "3.8.0", + "@docusaurus/types": "3.8.0", + "@docusaurus/utils-validation": "3.8.0", "tslib": "^2.6.0" }, "engines": { @@ -3474,14 +3489,14 @@ } }, "node_modules/@docusaurus/plugin-google-gtag": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.7.0.tgz", - "integrity": "sha512-M3vrMct1tY65ModbyeDaMoA+fNJTSPe5qmchhAbtqhDD/iALri0g9LrEpIOwNaoLmm6lO88sfBUADQrSRSGSWA==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.8.0.tgz", + "integrity": "sha512-ugQYMGF4BjbAW/JIBtVcp+9eZEgT9HRdvdcDudl5rywNPBA0lct+lXMG3r17s02rrhInMpjMahN3Yc9Cb3H5/g==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/core": "3.8.0", + "@docusaurus/types": "3.8.0", + "@docusaurus/utils-validation": "3.8.0", "@types/gtag.js": "^0.0.12", "tslib": "^2.6.0" }, @@ -3494,14 +3509,14 @@ } }, "node_modules/@docusaurus/plugin-google-tag-manager": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.7.0.tgz", - "integrity": "sha512-X8U78nb8eiMiPNg3jb9zDIVuuo/rE1LjGDGu+5m5CX4UBZzjMy+klOY2fNya6x8ACyE/L3K2erO1ErheP55W/w==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.8.0.tgz", + "integrity": "sha512-9juRWxbwZD3SV02Jd9QB6yeN7eu+7T4zB0bvJLcVQwi+am51wAxn2CwbdL0YCCX+9OfiXbADE8D8Q65Hbopu/w==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/core": "3.8.0", + "@docusaurus/types": "3.8.0", + "@docusaurus/utils-validation": "3.8.0", "tslib": "^2.6.0" }, "engines": { @@ -3513,17 +3528,17 @@ } }, "node_modules/@docusaurus/plugin-sitemap": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.7.0.tgz", - "integrity": "sha512-bTRT9YLZ/8I/wYWKMQke18+PF9MV8Qub34Sku6aw/vlZ/U+kuEuRpQ8bTcNOjaTSfYsWkK4tTwDMHK2p5S86cA==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.8.0.tgz", + "integrity": "sha512-fGpOIyJvNiuAb90nSJ2Gfy/hUOaDu6826e5w5UxPmbpCIc7KlBHNAZ5g4L4ZuHhc4hdfq4mzVBsQSnne+8Ze1g==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/core": "3.8.0", + "@docusaurus/logger": "3.8.0", + "@docusaurus/types": "3.8.0", + "@docusaurus/utils": "3.8.0", + "@docusaurus/utils-common": "3.8.0", + "@docusaurus/utils-validation": "3.8.0", "fs-extra": "^11.1.1", "sitemap": "^7.1.1", "tslib": "^2.6.0" @@ -3537,15 +3552,15 @@ } }, "node_modules/@docusaurus/plugin-svgr": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-svgr/-/plugin-svgr-3.7.0.tgz", - "integrity": "sha512-HByXIZTbc4GV5VAUkZ2DXtXv1Qdlnpk3IpuImwSnEzCDBkUMYcec5282hPjn6skZqB25M1TYCmWS91UbhBGxQg==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-svgr/-/plugin-svgr-3.8.0.tgz", + "integrity": "sha512-kEDyry+4OMz6BWLG/lEqrNsL/w818bywK70N1gytViw4m9iAmoxCUT7Ri9Dgs7xUdzCHJ3OujolEmD88Wy44OA==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/core": "3.8.0", + "@docusaurus/types": "3.8.0", + "@docusaurus/utils": "3.8.0", + "@docusaurus/utils-validation": "3.8.0", "@svgr/core": "8.1.0", "@svgr/webpack": "^8.1.0", "tslib": "^2.6.0", @@ -3560,25 +3575,26 @@ } }, "node_modules/@docusaurus/preset-classic": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.7.0.tgz", - "integrity": "sha512-nPHj8AxDLAaQXs+O6+BwILFuhiWbjfQWrdw2tifOClQoNfuXDjfjogee6zfx6NGHWqshR23LrcN115DmkHC91Q==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.8.0.tgz", + "integrity": "sha512-qOu6tQDOWv+rpTlKu+eJATCJVGnABpRCPuqf7LbEaQ1mNY//N/P8cHQwkpAU+aweQfarcZ0XfwCqRHJfjeSV/g==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/plugin-content-blog": "3.7.0", - "@docusaurus/plugin-content-docs": "3.7.0", - "@docusaurus/plugin-content-pages": "3.7.0", - "@docusaurus/plugin-debug": "3.7.0", - "@docusaurus/plugin-google-analytics": "3.7.0", - "@docusaurus/plugin-google-gtag": "3.7.0", - "@docusaurus/plugin-google-tag-manager": "3.7.0", - "@docusaurus/plugin-sitemap": "3.7.0", - "@docusaurus/plugin-svgr": "3.7.0", - "@docusaurus/theme-classic": "3.7.0", - "@docusaurus/theme-common": "3.7.0", - "@docusaurus/theme-search-algolia": "3.7.0", - "@docusaurus/types": "3.7.0" + "@docusaurus/core": "3.8.0", + "@docusaurus/plugin-content-blog": "3.8.0", + "@docusaurus/plugin-content-docs": "3.8.0", + "@docusaurus/plugin-content-pages": "3.8.0", + "@docusaurus/plugin-css-cascade-layers": "3.8.0", + "@docusaurus/plugin-debug": "3.8.0", + "@docusaurus/plugin-google-analytics": "3.8.0", + "@docusaurus/plugin-google-gtag": "3.8.0", + "@docusaurus/plugin-google-tag-manager": "3.8.0", + "@docusaurus/plugin-sitemap": "3.8.0", + "@docusaurus/plugin-svgr": "3.8.0", + "@docusaurus/theme-classic": "3.8.0", + "@docusaurus/theme-common": "3.8.0", + "@docusaurus/theme-search-algolia": "3.8.0", + "@docusaurus/types": "3.8.0" }, "engines": { "node": ">=18.0" @@ -3589,24 +3605,24 @@ } }, "node_modules/@docusaurus/theme-classic": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.7.0.tgz", - "integrity": "sha512-MnLxG39WcvLCl4eUzHr0gNcpHQfWoGqzADCly54aqCofQX6UozOS9Th4RK3ARbM9m7zIRv3qbhggI53dQtx/hQ==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.8.0.tgz", + "integrity": "sha512-nQWFiD5ZjoT76OaELt2n33P3WVuuCz8Dt5KFRP2fCBo2r9JCLsp2GJjZpnaG24LZ5/arRjv4VqWKgpK0/YLt7g==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/mdx-loader": "3.7.0", - "@docusaurus/module-type-aliases": "3.7.0", - "@docusaurus/plugin-content-blog": "3.7.0", - "@docusaurus/plugin-content-docs": "3.7.0", - "@docusaurus/plugin-content-pages": "3.7.0", - "@docusaurus/theme-common": "3.7.0", - "@docusaurus/theme-translations": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docusaurus/core": "3.8.0", + "@docusaurus/logger": "3.8.0", + "@docusaurus/mdx-loader": "3.8.0", + "@docusaurus/module-type-aliases": "3.8.0", + "@docusaurus/plugin-content-blog": "3.8.0", + "@docusaurus/plugin-content-docs": "3.8.0", + "@docusaurus/plugin-content-pages": "3.8.0", + "@docusaurus/theme-common": "3.8.0", + "@docusaurus/theme-translations": "3.8.0", + "@docusaurus/types": "3.8.0", + "@docusaurus/utils": "3.8.0", + "@docusaurus/utils-common": "3.8.0", + "@docusaurus/utils-validation": "3.8.0", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", "copy-text-to-clipboard": "^3.2.0", @@ -3630,15 +3646,15 @@ } }, "node_modules/@docusaurus/theme-common": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.7.0.tgz", - "integrity": "sha512-8eJ5X0y+gWDsURZnBfH0WabdNm8XMCXHv8ENy/3Z/oQKwaB/EHt5lP9VsTDTf36lKEp0V6DjzjFyFIB+CetL0A==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.8.0.tgz", + "integrity": "sha512-YqV2vAWpXGLA+A3PMLrOMtqgTHJLDcT+1Caa6RF7N4/IWgrevy5diY8oIHFkXR/eybjcrFFjUPrHif8gSGs3Tw==", "license": "MIT", "dependencies": { - "@docusaurus/mdx-loader": "3.7.0", - "@docusaurus/module-type-aliases": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", + "@docusaurus/mdx-loader": "3.8.0", + "@docusaurus/module-type-aliases": "3.8.0", + "@docusaurus/utils": "3.8.0", + "@docusaurus/utils-common": "3.8.0", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", @@ -3658,19 +3674,19 @@ } }, "node_modules/@docusaurus/theme-search-algolia": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.7.0.tgz", - "integrity": "sha512-Al/j5OdzwRU1m3falm+sYy9AaB93S1XF1Lgk9Yc6amp80dNxJVplQdQTR4cYdzkGtuQqbzUA8+kaoYYO0RbK6g==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.8.0.tgz", + "integrity": "sha512-GBZ5UOcPgiu6nUw153+0+PNWvFKweSnvKIL6Rp04H9olKb475jfKjAwCCtju5D2xs5qXHvCMvzWOg5o9f6DtuQ==", "license": "MIT", "dependencies": { - "@docsearch/react": "^3.8.1", - "@docusaurus/core": "3.7.0", - "@docusaurus/logger": "3.7.0", - "@docusaurus/plugin-content-docs": "3.7.0", - "@docusaurus/theme-common": "3.7.0", - "@docusaurus/theme-translations": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-validation": "3.7.0", + "@docsearch/react": "^3.9.0", + "@docusaurus/core": "3.8.0", + "@docusaurus/logger": "3.8.0", + "@docusaurus/plugin-content-docs": "3.8.0", + "@docusaurus/theme-common": "3.8.0", + "@docusaurus/theme-translations": "3.8.0", + "@docusaurus/utils": "3.8.0", + "@docusaurus/utils-validation": "3.8.0", "algoliasearch": "^5.17.1", "algoliasearch-helper": "^3.22.6", "clsx": "^2.0.0", @@ -3689,9 +3705,9 @@ } }, "node_modules/@docusaurus/theme-translations": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.7.0.tgz", - "integrity": "sha512-Ewq3bEraWDmienM6eaNK7fx+/lHMtGDHQyd1O+4+3EsDxxUmrzPkV7Ct3nBWTuE0MsoZr3yNwQVKjllzCMuU3g==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.8.0.tgz", + "integrity": "sha512-1DTy/snHicgkCkryWq54fZvsAglTdjTx4qjOXgqnXJ+DIty1B+aPQrAVUu8LiM+6BiILfmNxYsxhKTj+BS3PZg==", "license": "MIT", "dependencies": { "fs-extra": "^11.1.1", @@ -3702,16 +3718,16 @@ } }, "node_modules/@docusaurus/tsconfig": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/tsconfig/-/tsconfig-3.7.0.tgz", - "integrity": "sha512-vRsyj3yUZCjscgfgcFYjIsTcAru/4h4YH2/XAE8Rs7wWdnng98PgWKvP5ovVc4rmRpRg2WChVW0uOy2xHDvDBQ==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/tsconfig/-/tsconfig-3.8.0.tgz", + "integrity": "sha512-utLl48nNjSYBoq47RKukZ9fPLEX3nJWThzrujb0ndQQ1jc/gh4RhTRaAqItH9nImnsgGKmLMnyoMBpfGmoop+w==", "dev": true, "license": "MIT" }, "node_modules/@docusaurus/types": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.7.0.tgz", - "integrity": "sha512-kOmZg5RRqJfH31m+6ZpnwVbkqMJrPOG5t0IOl4i/+3ruXyNfWzZ0lVtVrD0u4ONc/0NOsS9sWYaxxWNkH1LdLQ==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.8.0.tgz", + "integrity": "sha512-RDEClpwNxZq02c+JlaKLWoS13qwWhjcNsi2wG1UpzmEnuti/z1Wx4SGpqbUqRPNSd8QWWePR8Cb7DvG0VN/TtA==", "license": "MIT", "dependencies": { "@mdx-js/mdx": "^3.0.0", @@ -3744,15 +3760,16 @@ } }, "node_modules/@docusaurus/utils": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.7.0.tgz", - "integrity": "sha512-e7zcB6TPnVzyUaHMJyLSArKa2AG3h9+4CfvKXKKWNx6hRs+p0a+u7HHTJBgo6KW2m+vqDnuIHK4X+bhmoghAFA==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.8.0.tgz", + "integrity": "sha512-2wvtG28ALCN/A1WCSLxPASFBFzXCnP0YKCAFIPcvEb6imNu1wg7ni/Svcp71b3Z2FaOFFIv4Hq+j4gD7gA0yfQ==", "license": "MIT", "dependencies": { - "@docusaurus/logger": "3.7.0", - "@docusaurus/types": "3.7.0", - "@docusaurus/utils-common": "3.7.0", + "@docusaurus/logger": "3.8.0", + "@docusaurus/types": "3.8.0", + "@docusaurus/utils-common": "3.8.0", "escape-string-regexp": "^4.0.0", + "execa": "5.1.1", "file-loader": "^6.2.0", "fs-extra": "^11.1.1", "github-slugger": "^1.5.0", @@ -3762,9 +3779,9 @@ "js-yaml": "^4.1.0", "lodash": "^4.17.21", "micromatch": "^4.0.5", + "p-queue": "^6.6.2", "prompts": "^2.4.2", "resolve-pathname": "^3.0.0", - "shelljs": "^0.8.5", "tslib": "^2.6.0", "url-loader": "^4.1.1", "utility-types": "^3.10.0", @@ -3775,12 +3792,12 @@ } }, "node_modules/@docusaurus/utils-common": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.7.0.tgz", - "integrity": "sha512-IZeyIfCfXy0Mevj6bWNg7DG7B8G+S6o6JVpddikZtWyxJguiQ7JYr0SIZ0qWd8pGNuMyVwriWmbWqMnK7Y5PwA==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.8.0.tgz", + "integrity": "sha512-3TGF+wVTGgQ3pAc9+5jVchES4uXUAhAt9pwv7uws4mVOxL4alvU3ue/EZ+R4XuGk94pDy7CNXjRXpPjlfZXQfw==", "license": "MIT", "dependencies": { - "@docusaurus/types": "3.7.0", + "@docusaurus/types": "3.8.0", "tslib": "^2.6.0" }, "engines": { @@ -3788,14 +3805,14 @@ } }, "node_modules/@docusaurus/utils-validation": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.7.0.tgz", - "integrity": "sha512-w8eiKk8mRdN+bNfeZqC4nyFoxNyI1/VExMKAzD9tqpJfLLbsa46Wfn5wcKH761g9WkKh36RtFV49iL9lh1DYBA==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.8.0.tgz", + "integrity": "sha512-MrnEbkigr54HkdFeg8e4FKc4EF+E9dlVwsY3XQZsNkbv3MKZnbHQ5LsNJDIKDROFe8PBf5C4qCAg5TPBpsjrjg==", "license": "MIT", "dependencies": { - "@docusaurus/logger": "3.7.0", - "@docusaurus/utils": "3.7.0", - "@docusaurus/utils-common": "3.7.0", + "@docusaurus/logger": "3.8.0", + "@docusaurus/utils": "3.8.0", + "@docusaurus/utils-common": "3.8.0", "fs-extra": "^11.2.0", "joi": "^17.9.2", "js-yaml": "^4.1.0", @@ -4757,12 +4774,6 @@ "@types/node": "*" } }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", - "license": "MIT" - }, "node_modules/@types/parse5": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-5.0.3.tgz", @@ -5437,15 +5448,6 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "license": "ISC", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/autocomplete.js": { "version": "0.37.1", "resolved": "https://registry.npmjs.org/autocomplete.js/-/autocomplete.js-0.37.1.tgz", @@ -7369,28 +7371,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/del": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", - "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==", - "license": "MIT", - "dependencies": { - "globby": "^11.0.1", - "graceful-fs": "^4.2.4", - "is-glob": "^4.0.1", - "is-path-cwd": "^2.2.0", - "is-path-inside": "^3.0.2", - "p-map": "^4.0.0", - "rimraf": "^3.0.2", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -7463,38 +7443,6 @@ "node": ">= 4.0.0" } }, - "node_modules/detect-port-alt": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", - "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==", - "license": "MIT", - "dependencies": { - "address": "^1.0.1", - "debug": "^2.6.0" - }, - "bin": { - "detect": "bin/detect-port", - "detect-port": "bin/detect-port" - }, - "engines": { - "node": ">= 4.2.1" - } - }, - "node_modules/detect-port-alt/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/detect-port-alt/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -8685,15 +8633,6 @@ "node": ">=0.10.0" } }, - "node_modules/filesize": { - "version": "8.0.7", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", - "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -8834,134 +8773,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/fork-ts-checker-webpack-plugin": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz", - "integrity": "sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.8.3", - "@types/json-schema": "^7.0.5", - "chalk": "^4.1.0", - "chokidar": "^3.4.2", - "cosmiconfig": "^6.0.0", - "deepmerge": "^4.2.2", - "fs-extra": "^9.0.0", - "glob": "^7.1.6", - "memfs": "^3.1.2", - "minimatch": "^3.0.4", - "schema-utils": "2.7.0", - "semver": "^7.3.2", - "tapable": "^1.0.0" - }, - "engines": { - "node": ">=10", - "yarn": ">=1.0.0" - }, - "peerDependencies": { - "eslint": ">= 6", - "typescript": ">= 2.7", - "vue-template-compiler": "*", - "webpack": ">= 4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - }, - "vue-template-compiler": { - "optional": true - } - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", - "license": "MIT", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.7.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "license": "MIT" - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", - "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.4", - "ajv": "^6.12.2", - "ajv-keywords": "^3.4.1" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/form-data": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", @@ -9281,44 +9092,6 @@ "node": ">=10" } }, - "node_modules/global-modules": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", - "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", - "license": "MIT", - "dependencies": { - "global-prefix": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/global-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", - "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", - "license": "MIT", - "dependencies": { - "ini": "^1.3.5", - "kind-of": "^6.0.2", - "which": "^1.3.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/global-prefix/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -10644,13 +10417,10 @@ } }, "node_modules/image-size": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", - "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz", + "integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==", "license": "MIT", - "dependencies": { - "queue": "6.0.2" - }, "bin": { "image-size": "bin/image-size.js" }, @@ -10994,15 +10764,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -11045,15 +10806,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-root": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", - "integrity": "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -14650,6 +14402,15 @@ "node": ">=12.20" } }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/p-limit": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", @@ -14695,6 +14456,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-retry": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", @@ -14708,13 +14485,16 @@ "node": ">=8" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, "engines": { - "node": ">=6" + "node": ">=8" } }, "node_modules/package-json": { @@ -15012,83 +14792,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/pkg-up": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", - "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", - "license": "MIT", - "dependencies": { - "find-up": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-up/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "license": "MIT", - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "license": "MIT", - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-up/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "license": "MIT", - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz", + "integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==", "funding": [ { "type": "opencollective", @@ -15105,7 +14812,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -16894,15 +16601,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/queue": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", - "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", - "license": "MIT", - "dependencies": { - "inherits": "~2.0.3" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -17094,132 +16792,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-dev-utils": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", - "integrity": "sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.16.0", - "address": "^1.1.2", - "browserslist": "^4.18.1", - "chalk": "^4.1.2", - "cross-spawn": "^7.0.3", - "detect-port-alt": "^1.1.6", - "escape-string-regexp": "^4.0.0", - "filesize": "^8.0.6", - "find-up": "^5.0.0", - "fork-ts-checker-webpack-plugin": "^6.5.0", - "global-modules": "^2.0.0", - "globby": "^11.0.4", - "gzip-size": "^6.0.0", - "immer": "^9.0.7", - "is-root": "^2.1.0", - "loader-utils": "^3.2.0", - "open": "^8.4.0", - "pkg-up": "^3.1.0", - "prompts": "^2.4.2", - "react-error-overlay": "^6.0.11", - "recursive-readdir": "^2.2.2", - "shell-quote": "^1.7.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/react-dev-utils/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/loader-utils": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", - "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/react-dev-utils/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-dev-utils/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/react-dev-utils/node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -17233,12 +16805,6 @@ "react": "^18.3.1" } }, - "node_modules/react-error-overlay": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.1.0.tgz", - "integrity": "sha512-SN/U6Ytxf1QGkw/9ve5Y+NxBbZM6Ht95tuXNMKs8EJyFa/Vy/+Co3stop3KBHARfn/giv+Lj1uUnTfOJ3moFEQ==", - "license": "MIT" - }, "node_modules/react-fast-compare": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", @@ -17270,15 +16836,15 @@ "license": "MIT" }, "node_modules/react-json-view-lite": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/react-json-view-lite/-/react-json-view-lite-1.5.0.tgz", - "integrity": "sha512-nWqA1E4jKPklL2jvHWs6s+7Na0qNgw9HCP6xehdQJeg6nPBTFZgGwyko9Q0oj+jQWKTTVRS30u0toM5wiuL3iw==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/react-json-view-lite/-/react-json-view-lite-2.4.1.tgz", + "integrity": "sha512-fwFYknRIBxjbFm0kBDrzgBy1xa5tDg2LyXXBepC5f1b+MY3BUClMCsvanMPn089JbV1Eg3nZcrp0VCuH43aXnA==", "license": "MIT", "engines": { - "node": ">=14" + "node": ">=18" }, "peerDependencies": { - "react": "^16.13.1 || ^17.0.0 || ^18.0.0" + "react": "^18.0.0 || ^19.0.0" } }, "node_modules/react-loadable": { @@ -17433,12 +16999,6 @@ "node": ">=8.10.0" } }, - "node_modules/reading-time": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/reading-time/-/reading-time-1.5.0.tgz", - "integrity": "sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==", - "license": "MIT" - }, "node_modules/rechoir": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", @@ -17514,18 +17074,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/recursive-readdir": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", - "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", - "license": "MIT", - "dependencies": { - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/redux": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", @@ -18208,6 +17756,12 @@ "loose-envify": "^1.1.0" } }, + "node_modules/schema-dts": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/schema-dts/-/schema-dts-1.1.5.tgz", + "integrity": "sha512-RJr9EaCmsLzBX2NDiO5Z3ux2BVosNZN5jo0gWgsyKvxKIUL5R3swNvoorulAeL9kLB0iTSX7V6aokhla2m7xbg==", + "license": "Apache-2.0" + }, "node_modules/schema-utils": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", @@ -19472,12 +19026,6 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT" }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "license": "MIT" - }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -19517,6 +19065,15 @@ "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", "license": "MIT" }, + "node_modules/tinypool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.0.tgz", + "integrity": "sha512-7CotroY9a8DKsKprEy/a14aCCm8jYVmR7aFy4fpkZM8sdpNJbKkixuNjgM50yCmip2ezc8z4N7k3oe2+rfRJCQ==", + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -19686,6 +19243,7 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/docs/package.json b/docs/package.json index e13e85ecb33..2cc81f1b2c1 100644 --- a/docs/package.json +++ b/docs/package.json @@ -16,8 +16,8 @@ "write-heading-ids": "docusaurus write-heading-ids" }, "dependencies": { - "@docusaurus/core": "~3.7.0", - "@docusaurus/preset-classic": "~3.7.0", + "@docusaurus/core": "~3.8.0", + "@docusaurus/preset-classic": "~3.8.0", "@mdi/js": "^7.3.67", "@mdi/react": "^1.6.1", "@mdx-js/react": "^3.0.0", @@ -35,7 +35,7 @@ "url": "^0.11.0" }, "devDependencies": { - "@docusaurus/module-type-aliases": "~3.7.0", + "@docusaurus/module-type-aliases": "~3.8.0", "@docusaurus/tsconfig": "^3.7.0", "@docusaurus/types": "^3.7.0", "prettier": "^3.2.4", diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 343295c60d7..6d837d67ed4 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1709,17 +1709,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", - "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.0.tgz", + "integrity": "sha512-CACyQuqSHt7ma3Ns601xykeBK/rDeZa3w6IS6UtMQbixO5DWy+8TilKkviGDH6jtWCo8FGRKEK5cLLkPvEammQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/type-utils": "8.32.1", - "@typescript-eslint/utils": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/type-utils": "8.33.0", + "@typescript-eslint/utils": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -1733,7 +1733,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "@typescript-eslint/parser": "^8.33.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } @@ -1749,16 +1749,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", - "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.0.tgz", + "integrity": "sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/typescript-estree": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "debug": "^4.3.4" }, "engines": { @@ -1773,15 +1773,16 @@ "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", - "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.0.tgz", + "integrity": "sha512-d1hz0u9l6N+u/gcrk6s6gYdl7/+pp8yHheRTqP6X5hVDKALEaTn8WfGiit7G511yueBEL3OpOEpD+3/MBdoN+A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1" + "@typescript-eslint/tsconfig-utils": "^8.33.0", + "@typescript-eslint/types": "^8.33.0", + "debug": "^4.3.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1791,15 +1792,50 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", - "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.0.tgz", + "integrity": "sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/utils": "8.32.1", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.0.tgz", + "integrity": "sha512-sTkETlbqhEoiFmGr1gsdq5HyVbSOF0145SYDJ/EQmXHtKViCaGvnyLqWFFHtEXoS0J1yU8Wyou2UGmgW88fEug==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.33.0.tgz", + "integrity": "sha512-lScnHNCBqL1QayuSrWeqAL5GmqNdVUQAAMTaCwdYEdWfIrSrOGzyLGRCHXcCixa5NK6i5l0AfSO2oBSjCjf4XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.33.0", + "@typescript-eslint/utils": "8.33.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1816,9 +1852,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", - "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.0.tgz", + "integrity": "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg==", "dev": true, "license": "MIT", "engines": { @@ -1830,14 +1866,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", - "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.0.tgz", + "integrity": "sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/project-service": "8.33.0", + "@typescript-eslint/tsconfig-utils": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1883,16 +1921,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", - "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.33.0.tgz", + "integrity": "sha512-lPFuQaLA9aSNa7D5u2EpRiqdAUhzShwGg/nhpBlc4GR6kcTABttCuyjFs8BcEZ8VWrjCBof/bePhP3Q3fS+Yrw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1" + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/typescript-estree": "8.33.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1907,13 +1945,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", - "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.0.tgz", + "integrity": "sha512-7RW7CMYoskiz5OOGAWjJFxgb7c5UNjTG292gYhWeOAcFmYCtVCSqjqSBj5zMhxbXo2JOW95YYrUWJfU0zrpaGQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/types": "8.33.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -3834,9 +3872,9 @@ } }, "node_modules/globals": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz", - "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", + "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", "dev": true, "license": "MIT", "engines": { @@ -6779,15 +6817,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz", - "integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.33.0.tgz", + "integrity": "sha512-5YmNhF24ylCsvdNW2oJwMzTbaeO4bg90KeGtMjUw0AGtHksgEPLRTUil+coHwCfiu4QjVJFnjp94DmU6zV7DhQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.32.1", - "@typescript-eslint/parser": "8.32.1", - "@typescript-eslint/utils": "8.32.1" + "@typescript-eslint/eslint-plugin": "8.33.0", + "@typescript-eslint/parser": "8.33.0", + "@typescript-eslint/utils": "8.33.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/server/package-lock.json b/server/package-lock.json index 8df5b7f2497..592ce857f91 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -2349,12 +2349,12 @@ } }, "node_modules/@nestjs/common": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.1.tgz", - "integrity": "sha512-crzp+1qeZ5EGL0nFTPy9NrVMAaUWewV5AwtQyv6SQ9yQPXwRl9W9hm1pt0nAtUu5QbYMbSuo7lYcF81EjM+nCA==", + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.2.tgz", + "integrity": "sha512-cHh4OPH44PjaHM93D1jgE1HO/B7XTZVRDxy/cPuGgyMEA4p2zXO+qqcOgTMC5FYcp7dX9jLeCjXAU0ToFAnODw==", "license": "MIT", "dependencies": { - "file-type": "20.5.0", + "file-type": "21.0.0", "iterare": "1.2.1", "load-esm": "1.0.2", "tslib": "2.8.1", @@ -2380,9 +2380,9 @@ } }, "node_modules/@nestjs/core": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.1.tgz", - "integrity": "sha512-UFoUAgLKFT+RwHTANJdr0dF7p0qS9QjkaUPjg8aafnjM/qxxxrUVDB49nVvyMlk+Hr1+vvcNaOHbWWQBxoZcHA==", + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.2.tgz", + "integrity": "sha512-QRuyxwu0BjNfmmmunsw1ylX7RSyfDQHt+xD+tKncdtgiMOOzAu+LA1gB4WoZnw4frQkk+qZbhEbM61cIjOxD3w==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -2454,14 +2454,14 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.1.tgz", - "integrity": "sha512-IUxk380qnUtz0PCRQ5i+o9UHlGMrFzGPIJxDwyt3JZZwx2AngOlcEcm5e+7YeJQEr2QYX2QyC4tUQg0zde+D7A==", + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.2.tgz", + "integrity": "sha512-GlNwOT4htRp8RpZ+TpqGtSHwGKw/abdxxBRse40XE2SWs5ikaoujr9Yd+5sJWDNXB4QTftwb+FplXhyk1Ra+4A==", "license": "MIT", "dependencies": { "cors": "2.8.5", "express": "5.1.0", - "multer": "1.4.5-lts.2", + "multer": "2.0.0", "path-to-regexp": "8.2.0", "tslib": "2.8.1" }, @@ -2475,9 +2475,9 @@ } }, "node_modules/@nestjs/platform-socket.io": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.1.1.tgz", - "integrity": "sha512-Bsc8ouysUFasWiO8RKEvppqYM5LNkHfbyIJQTy3V6+PUdYhblkvmOq8QtjuHpv6DiBI4siUcxACx/90/CdXLkQ==", + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.1.2.tgz", + "integrity": "sha512-IkeDPRRddY0In6lE+5H/DJodtF5cEx+ga+GWehs4Il5Y3kK7MVR2/WgUABAhyRsbJYOhIhZD7Dai0V2t9ref1Q==", "license": "MIT", "dependencies": { "socket.io": "4.8.1", @@ -2663,9 +2663,9 @@ } }, "node_modules/@nestjs/testing": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.1.tgz", - "integrity": "sha512-stzm8YrLDGAijHYQw+8Z9dD6lGdvahL0hIjGVZ/0KBxLZht0/rvRjgV31UK+DUqXaF7yhJTw9ryrPaITxI1J6A==", + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.2.tgz", + "integrity": "sha512-BQxVKUVW6gzEbbHAvmg5RgcP3s++pRgTCmsgaDF/DtcLRUeKi8SjAdqzLm14xbkMeibxOf3fNqM2iwqUKj8ffw==", "dev": true, "license": "MIT", "dependencies": { @@ -2691,9 +2691,9 @@ } }, "node_modules/@nestjs/websockets": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.1.1.tgz", - "integrity": "sha512-gxwQoGx5bW5IvparzrX1UOGXz87eqY0fK5Y6yb14z6tSSubQTciNjCDm5osDEkRyRCG6ZB0F+eXF6dRUjwTlBQ==", + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.1.2.tgz", + "integrity": "sha512-Ywl7u0C3+qnKIrk0mD3jHWnowO+GScFT1FeP6cNgarA0ujHEfusph9IIbnUJiEiusfnKVpK9fYMGZRSDwnRGPQ==", "license": "MIT", "dependencies": { "iterare": "1.2.1", @@ -5576,9 +5576,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.1.5", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.5.tgz", - "integrity": "sha512-piErsCVVbpMMT2r7wbawdZsq4xMvIAhQuac2gedQHysu1TZYEigE6pnFfgZT+/jQnrRuF5r+SHzuehFjfRjr4g==", + "version": "19.1.6", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.6.tgz", + "integrity": "sha512-JeG0rEWak0N6Itr6QUx+X60uQmN+5t3j9r/OVDtWzFXKaj6kD1BwJzOksD0FF6iWxZlbE1kB0q9vtnU2ekqa1Q==", "dev": true, "license": "MIT", "dependencies": { @@ -5735,17 +5735,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", - "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.0.tgz", + "integrity": "sha512-CACyQuqSHt7ma3Ns601xykeBK/rDeZa3w6IS6UtMQbixO5DWy+8TilKkviGDH6jtWCo8FGRKEK5cLLkPvEammQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/type-utils": "8.32.1", - "@typescript-eslint/utils": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/type-utils": "8.33.0", + "@typescript-eslint/utils": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -5759,7 +5759,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "@typescript-eslint/parser": "^8.33.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } @@ -5775,16 +5775,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", - "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.0.tgz", + "integrity": "sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/typescript-estree": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "debug": "^4.3.4" }, "engines": { @@ -5799,15 +5799,16 @@ "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", - "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.0.tgz", + "integrity": "sha512-d1hz0u9l6N+u/gcrk6s6gYdl7/+pp8yHheRTqP6X5hVDKALEaTn8WfGiit7G511yueBEL3OpOEpD+3/MBdoN+A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1" + "@typescript-eslint/tsconfig-utils": "^8.33.0", + "@typescript-eslint/types": "^8.33.0", + "debug": "^4.3.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5817,15 +5818,50 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", - "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.0.tgz", + "integrity": "sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/utils": "8.32.1", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.0.tgz", + "integrity": "sha512-sTkETlbqhEoiFmGr1gsdq5HyVbSOF0145SYDJ/EQmXHtKViCaGvnyLqWFFHtEXoS0J1yU8Wyou2UGmgW88fEug==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.33.0.tgz", + "integrity": "sha512-lScnHNCBqL1QayuSrWeqAL5GmqNdVUQAAMTaCwdYEdWfIrSrOGzyLGRCHXcCixa5NK6i5l0AfSO2oBSjCjf4XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.33.0", + "@typescript-eslint/utils": "8.33.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -5842,9 +5878,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", - "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.0.tgz", + "integrity": "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg==", "dev": true, "license": "MIT", "engines": { @@ -5856,14 +5892,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", - "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.0.tgz", + "integrity": "sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/project-service": "8.33.0", + "@typescript-eslint/tsconfig-utils": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -5909,16 +5947,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", - "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.33.0.tgz", + "integrity": "sha512-lPFuQaLA9aSNa7D5u2EpRiqdAUhzShwGg/nhpBlc4GR6kcTABttCuyjFs8BcEZ8VWrjCBof/bePhP3Q3fS+Yrw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1" + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/typescript-estree": "8.33.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5933,13 +5971,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", - "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.0.tgz", + "integrity": "sha512-7RW7CMYoskiz5OOGAWjJFxgb7c5UNjTG292gYhWeOAcFmYCtVCSqjqSBj5zMhxbXo2JOW95YYrUWJfU0zrpaGQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/types": "8.33.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -9572,18 +9610,18 @@ } }, "node_modules/file-type": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.5.0.tgz", - "integrity": "sha512-BfHZtG/l9iMm4Ecianu7P8HRD2tBHLtjXinm4X62XBOYzi7CYA7jyqfJzOvXHqzVrVPYqBo2/GvbARMaaJkKVg==", + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.0.0.tgz", + "integrity": "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==", "license": "MIT", "dependencies": { - "@tokenizer/inflate": "^0.2.6", - "strtok3": "^10.2.0", + "@tokenizer/inflate": "^0.2.7", + "strtok3": "^10.2.2", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sindresorhus/file-type?sponsor=1" @@ -10201,9 +10239,9 @@ } }, "node_modules/globals": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz", - "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", + "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", "dev": true, "license": "MIT", "engines": { @@ -12125,9 +12163,9 @@ } }, "node_modules/multer": { - "version": "1.4.5-lts.2", - "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", - "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.0.tgz", + "integrity": "sha512-bS8rPZurbAuHGAnApbM9d4h1wSoYqrOqkE+6a64KLMK9yWU7gJXBDDVklKQ3TPi9DRb85cRs6yXaC0+cjxRtRg==", "license": "MIT", "dependencies": { "append-field": "^1.0.0", @@ -12139,7 +12177,7 @@ "xtend": "^4.0.0" }, "engines": { - "node": ">= 6.0.0" + "node": ">= 10.16.0" } }, "node_modules/multer/node_modules/concat-stream": { @@ -17052,15 +17090,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz", - "integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.33.0.tgz", + "integrity": "sha512-5YmNhF24ylCsvdNW2oJwMzTbaeO4bg90KeGtMjUw0AGtHksgEPLRTUil+coHwCfiu4QjVJFnjp94DmU6zV7DhQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.32.1", - "@typescript-eslint/parser": "8.32.1", - "@typescript-eslint/utils": "8.32.1" + "@typescript-eslint/eslint-plugin": "8.33.0", + "@typescript-eslint/parser": "8.33.0", + "@typescript-eslint/utils": "8.33.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -17379,9 +17417,9 @@ } }, "node_modules/validator": { - "version": "13.15.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.0.tgz", - "integrity": "sha512-36B2ryl4+oL5QxZ3AzD0t5SsMNGvTtQHpjgFO5tbNxfXbMFkY822ktCDe1MnlqV3301QQI9SLHDNJokDI+Z9pA==", + "version": "13.15.15", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", + "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", "license": "MIT", "engines": { "node": ">= 0.10" diff --git a/web/package-lock.json b/web/package-lock.json index ef3b1056608..10e4c0dc396 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -2229,9 +2229,9 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.7.tgz", - "integrity": "sha512-9rsOpdY9idRI2NH6CL4wORFY0+Q6fnx9XP9Ju+iq/0wJwGD5IByIgFmwVbyy4ymuyprj8Qh4ErxMKTUL4uNh3g==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.8.tgz", + "integrity": "sha512-OWwBsbC9BFAJelmnNcrKuf+bka2ZxCE2A4Ft53Tkg4uoiE67r/PMEYwCsourC26E+kmxfwE0hVzMdxqeW+xu7Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2241,13 +2241,13 @@ "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", - "tailwindcss": "4.1.7" + "tailwindcss": "4.1.8" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.7.tgz", - "integrity": "sha512-5SF95Ctm9DFiUyjUPnDGkoKItPX/k+xifcQhcqX5RA85m50jw1pT/KzjdvlqxRja45Y52nR4MR9fD1JYd7f8NQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.8.tgz", + "integrity": "sha512-d7qvv9PsM5N3VNKhwVUhpK6r4h9wtLkJ6lz9ZY9aeZgrUWk1Z8VPyqyDT9MZlem7GTGseRQHkeB1j3tC7W1P+A==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2259,24 +2259,24 @@ "node": ">= 10" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.7", - "@tailwindcss/oxide-darwin-arm64": "4.1.7", - "@tailwindcss/oxide-darwin-x64": "4.1.7", - "@tailwindcss/oxide-freebsd-x64": "4.1.7", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.7", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.7", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.7", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.7", - "@tailwindcss/oxide-linux-x64-musl": "4.1.7", - "@tailwindcss/oxide-wasm32-wasi": "4.1.7", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.7", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.7" + "@tailwindcss/oxide-android-arm64": "4.1.8", + "@tailwindcss/oxide-darwin-arm64": "4.1.8", + "@tailwindcss/oxide-darwin-x64": "4.1.8", + "@tailwindcss/oxide-freebsd-x64": "4.1.8", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.8", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.8", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.8", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.8", + "@tailwindcss/oxide-linux-x64-musl": "4.1.8", + "@tailwindcss/oxide-wasm32-wasi": "4.1.8", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.8", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.8" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.7.tgz", - "integrity": "sha512-IWA410JZ8fF7kACus6BrUwY2Z1t1hm0+ZWNEzykKmMNM09wQooOcN/VXr0p/WJdtHZ90PvJf2AIBS/Ceqx1emg==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.8.tgz", + "integrity": "sha512-Fbz7qni62uKYceWYvUjRqhGfZKwhZDQhlrJKGtnZfuNtHFqa8wmr+Wn74CTWERiW2hn3mN5gTpOoxWKk0jRxjg==", "cpu": [ "arm64" ], @@ -2291,9 +2291,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.7.tgz", - "integrity": "sha512-81jUw9To7fimGGkuJ2W5h3/oGonTOZKZ8C2ghm/TTxbwvfSiFSDPd6/A/KE2N7Jp4mv3Ps9OFqg2fEKgZFfsvg==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.8.tgz", + "integrity": "sha512-RdRvedGsT0vwVVDztvyXhKpsU2ark/BjgG0huo4+2BluxdXo8NDgzl77qh0T1nUxmM11eXwR8jA39ibvSTbi7A==", "cpu": [ "arm64" ], @@ -2308,9 +2308,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.7.tgz", - "integrity": "sha512-q77rWjEyGHV4PdDBtrzO0tgBBPlQWKY7wZK0cUok/HaGgbNKecegNxCGikuPJn5wFAlIywC3v+WMBt0PEBtwGw==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.8.tgz", + "integrity": "sha512-t6PgxjEMLp5Ovf7uMb2OFmb3kqzVTPPakWpBIFzppk4JE4ix0yEtbtSjPbU8+PZETpaYMtXvss2Sdkx8Vs4XRw==", "cpu": [ "x64" ], @@ -2325,9 +2325,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.7.tgz", - "integrity": "sha512-RfmdbbK6G6ptgF4qqbzoxmH+PKfP4KSVs7SRlTwcbRgBwezJkAO3Qta/7gDy10Q2DcUVkKxFLXUQO6J3CRvBGw==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.8.tgz", + "integrity": "sha512-g8C8eGEyhHTqwPStSwZNSrOlyx0bhK/V/+zX0Y+n7DoRUzyS8eMbVshVOLJTDDC+Qn9IJnilYbIKzpB9n4aBsg==", "cpu": [ "x64" ], @@ -2342,9 +2342,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.7.tgz", - "integrity": "sha512-OZqsGvpwOa13lVd1z6JVwQXadEobmesxQ4AxhrwRiPuE04quvZHWn/LnihMg7/XkN+dTioXp/VMu/p6A5eZP3g==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.8.tgz", + "integrity": "sha512-Jmzr3FA4S2tHhaC6yCjac3rGf7hG9R6Gf2z9i9JFcuyy0u79HfQsh/thifbYTF2ic82KJovKKkIB6Z9TdNhCXQ==", "cpu": [ "arm" ], @@ -2359,9 +2359,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.7.tgz", - "integrity": "sha512-voMvBTnJSfKecJxGkoeAyW/2XRToLZ227LxswLAwKY7YslG/Xkw9/tJNH+3IVh5bdYzYE7DfiaPbRkSHFxY1xA==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.8.tgz", + "integrity": "sha512-qq7jXtO1+UEtCmCeBBIRDrPFIVI4ilEQ97qgBGdwXAARrUqSn/L9fUrkb1XP/mvVtoVeR2bt/0L77xx53bPZ/Q==", "cpu": [ "arm64" ], @@ -2376,9 +2376,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.7.tgz", - "integrity": "sha512-PjGuNNmJeKHnP58M7XyjJyla8LPo+RmwHQpBI+W/OxqrwojyuCQ+GUtygu7jUqTEexejZHr/z3nBc/gTiXBj4A==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.8.tgz", + "integrity": "sha512-O6b8QesPbJCRshsNApsOIpzKt3ztG35gfX9tEf4arD7mwNinsoCKxkj8TgEE0YRjmjtO3r9FlJnT/ENd9EVefQ==", "cpu": [ "arm64" ], @@ -2393,9 +2393,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.7.tgz", - "integrity": "sha512-HMs+Va+ZR3gC3mLZE00gXxtBo3JoSQxtu9lobbZd+DmfkIxR54NO7Z+UQNPsa0P/ITn1TevtFxXTpsRU7qEvWg==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.8.tgz", + "integrity": "sha512-32iEXX/pXwikshNOGnERAFwFSfiltmijMIAbUhnNyjFr3tmWmMJWQKU2vNcFX0DACSXJ3ZWcSkzNbaKTdngH6g==", "cpu": [ "x64" ], @@ -2410,9 +2410,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.7.tgz", - "integrity": "sha512-MHZ6jyNlutdHH8rd+YTdr3QbXrHXqwIhHw9e7yXEBcQdluGwhpQY2Eku8UZK6ReLaWtQ4gijIv5QoM5eE+qlsA==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.8.tgz", + "integrity": "sha512-s+VSSD+TfZeMEsCaFaHTaY5YNj3Dri8rST09gMvYQKwPphacRG7wbuQ5ZJMIJXN/puxPcg/nU+ucvWguPpvBDg==", "cpu": [ "x64" ], @@ -2427,9 +2427,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.7.tgz", - "integrity": "sha512-ANaSKt74ZRzE2TvJmUcbFQ8zS201cIPxUDm5qez5rLEwWkie2SkGtA4P+GPTj+u8N6JbPrC8MtY8RmJA35Oo+A==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.8.tgz", + "integrity": "sha512-CXBPVFkpDjM67sS1psWohZ6g/2/cd+cq56vPxK4JeawelxwK4YECgl9Y9TjkE2qfF+9/s1tHHJqrC4SS6cVvSg==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -2448,7 +2448,7 @@ "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@emnapi/wasi-threads": "^1.0.2", - "@napi-rs/wasm-runtime": "^0.2.9", + "@napi-rs/wasm-runtime": "^0.2.10", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, @@ -2456,70 +2456,10 @@ "node": ">=14.0.0" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.4.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.0.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.4.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.0.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.9", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.0", - "@emnapi/runtime": "^1.4.0", - "@tybys/wasm-util": "^0.9.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.9.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.0", - "dev": true, - "inBundle": true, - "license": "0BSD", - "optional": true - }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.7.tgz", - "integrity": "sha512-HUiSiXQ9gLJBAPCMVRk2RT1ZrBjto7WvqsPBwUrNK2BcdSxMnk19h4pjZjI7zgPhDxlAbJSumTC4ljeA9y0tEw==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.8.tgz", + "integrity": "sha512-7GmYk1n28teDHUjPlIx4Z6Z4hHEgvP5ZW2QS9ygnDAdI/myh3HTHjDqtSqgu1BpRoI4OiLx+fThAyA1JePoENA==", "cpu": [ "arm64" ], @@ -2534,9 +2474,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.7.tgz", - "integrity": "sha512-rYHGmvoHiLJ8hWucSfSOEmdCBIGZIq7SpkPRSqLsH2Ab2YUNgKeAPT1Fi2cx3+hnYOrAb0jp9cRyode3bBW4mQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.8.tgz", + "integrity": "sha512-fou+U20j+Jl0EHwK92spoWISON2OBnCazIc038Xj2TdweYV33ZRkS9nwqiUi2d/Wba5xg5UoHfvynnb/UB49cQ==", "cpu": [ "x64" ], @@ -2628,15 +2568,15 @@ } }, "node_modules/@tailwindcss/vite": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.7.tgz", - "integrity": "sha512-tYa2fO3zDe41I7WqijyVbRd8oWT0aEID1Eokz5hMT6wShLIHj3yvwj9XbfuloHP9glZ6H+aG2AN/+ZrxJ1Y5RQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.8.tgz", + "integrity": "sha512-CQ+I8yxNV5/6uGaJjiuymgw0kEQiNKRinYbZXPdx1fk5WgiyReG0VaUx/Xq6aVNSUNJFzxm6o8FNKS5aMaim5A==", "dev": true, "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.1.7", - "@tailwindcss/oxide": "4.1.7", - "tailwindcss": "4.1.7" + "@tailwindcss/node": "4.1.8", + "@tailwindcss/oxide": "4.1.8", + "tailwindcss": "4.1.8" }, "peerDependencies": { "vite": "^5.2.0 || ^6" @@ -2948,17 +2888,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", - "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.0.tgz", + "integrity": "sha512-CACyQuqSHt7ma3Ns601xykeBK/rDeZa3w6IS6UtMQbixO5DWy+8TilKkviGDH6jtWCo8FGRKEK5cLLkPvEammQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/type-utils": "8.32.1", - "@typescript-eslint/utils": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/type-utils": "8.33.0", + "@typescript-eslint/utils": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -2972,7 +2912,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "@typescript-eslint/parser": "^8.33.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } @@ -2988,16 +2928,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", - "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.0.tgz", + "integrity": "sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/typescript-estree": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "debug": "^4.3.4" }, "engines": { @@ -3012,15 +2952,16 @@ "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", - "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.0.tgz", + "integrity": "sha512-d1hz0u9l6N+u/gcrk6s6gYdl7/+pp8yHheRTqP6X5hVDKALEaTn8WfGiit7G511yueBEL3OpOEpD+3/MBdoN+A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1" + "@typescript-eslint/tsconfig-utils": "^8.33.0", + "@typescript-eslint/types": "^8.33.0", + "debug": "^4.3.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3030,15 +2971,50 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", - "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.0.tgz", + "integrity": "sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/utils": "8.32.1", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.0.tgz", + "integrity": "sha512-sTkETlbqhEoiFmGr1gsdq5HyVbSOF0145SYDJ/EQmXHtKViCaGvnyLqWFFHtEXoS0J1yU8Wyou2UGmgW88fEug==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.33.0.tgz", + "integrity": "sha512-lScnHNCBqL1QayuSrWeqAL5GmqNdVUQAAMTaCwdYEdWfIrSrOGzyLGRCHXcCixa5NK6i5l0AfSO2oBSjCjf4XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.33.0", + "@typescript-eslint/utils": "8.33.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -3055,9 +3031,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", - "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.0.tgz", + "integrity": "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg==", "dev": true, "license": "MIT", "engines": { @@ -3069,14 +3045,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", - "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.0.tgz", + "integrity": "sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", + "@typescript-eslint/project-service": "8.33.0", + "@typescript-eslint/tsconfig-utils": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/visitor-keys": "8.33.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -3122,16 +3100,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", - "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.33.0.tgz", + "integrity": "sha512-lPFuQaLA9aSNa7D5u2EpRiqdAUhzShwGg/nhpBlc4GR6kcTABttCuyjFs8BcEZ8VWrjCBof/bePhP3Q3fS+Yrw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1" + "@typescript-eslint/scope-manager": "8.33.0", + "@typescript-eslint/types": "8.33.0", + "@typescript-eslint/typescript-estree": "8.33.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3146,13 +3124,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", - "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.0.tgz", + "integrity": "sha512-7RW7CMYoskiz5OOGAWjJFxgb7c5UNjTG292gYhWeOAcFmYCtVCSqjqSBj5zMhxbXo2JOW95YYrUWJfU0zrpaGQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/types": "8.33.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -4979,9 +4957,9 @@ } }, "node_modules/fabric": { - "version": "6.6.5", - "resolved": "https://registry.npmjs.org/fabric/-/fabric-6.6.5.tgz", - "integrity": "sha512-BFxyLDeLMMgtteqQwKAyRM+oSkf82lDFzsiC7AMob7k7ag7naFuHOtWtcll4v+M9Cpn5aqRBfz1shnsO0vZhbg==", + "version": "6.6.6", + "resolved": "https://registry.npmjs.org/fabric/-/fabric-6.6.6.tgz", + "integrity": "sha512-cL0m/RanEIiP67/TAj8kAQcEYlXofeB1SXCB1w7a0ktyUQHdRpnm2/VHlqsD/PfSLlGqftHzmxAS4LvKzSlrEw==", "license": "MIT", "engines": { "node": ">=16.20.0" @@ -5451,9 +5429,9 @@ } }, "node_modules/globals": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz", - "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", + "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", "dev": true, "license": "MIT", "engines": { @@ -8634,9 +8612,9 @@ } }, "node_modules/svelte": { - "version": "5.33.1", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.33.1.tgz", - "integrity": "sha512-7znzaaQALL62NBzkdKV04tmYIVla8qjrW+k6GdgFZcKcj8XOb8iEjmfRPo40iaWZlKv3+uiuc0h4iaGgwoORtA==", + "version": "5.33.6", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.33.6.tgz", + "integrity": "sha512-bxg2QY03JlrilCZmDlshY95Argj0rnX43UQFWZN4fct8PZTNBBmvfow2A6yOW1+YweDjhC2qdZF66ASI0Y21Tw==", "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", @@ -8885,9 +8863,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.7.tgz", - "integrity": "sha512-kr1o/ErIdNhTz8uzAYL7TpaUuzKIE6QPQ4qmSdxnoX/lo+5wmUHQA6h3L5yIqEImSRnAAURDirLu/BgiXGPAhg==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.8.tgz", + "integrity": "sha512-kjeW8gjdxasbmFKpVGrGd5T4i40mV5J2Rasw48QARfYeQ8YS9x02ON9SFWax3Qf616rt4Cp3nVNIj6Hd1mP3og==", "license": "MIT" }, "node_modules/tapable": { @@ -9211,15 +9189,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.32.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz", - "integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==", + "version": "8.33.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.33.0.tgz", + "integrity": "sha512-5YmNhF24ylCsvdNW2oJwMzTbaeO4bg90KeGtMjUw0AGtHksgEPLRTUil+coHwCfiu4QjVJFnjp94DmU6zV7DhQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.32.1", - "@typescript-eslint/parser": "8.32.1", - "@typescript-eslint/utils": "8.32.1" + "@typescript-eslint/eslint-plugin": "8.33.0", + "@typescript-eslint/parser": "8.33.0", + "@typescript-eslint/utils": "8.33.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" From e506c7fb19752a39ee09ad5db75ba5b063fb296d Mon Sep 17 00:00:00 2001 From: Thien Dang <89862334+dvbthien@users.noreply.github.com> Date: Tue, 3 Jun 2025 21:30:39 +0700 Subject: [PATCH 46/50] feat(mobile): Improve language setting UI (#18854) * improve language ui * fix lint * add search language, add safe area, fix button in dark * hide apply button when search not found --------- Co-authored-by: dvbthien --- i18n/en.json | 4 +- mobile/lib/pages/common/settings.page.dart | 2 +- .../widgets/settings/language_settings.dart | 365 +++++++++++++++--- 3 files changed, 309 insertions(+), 62 deletions(-) diff --git a/i18n/en.json b/i18n/en.json index 98ca467c51e..a25d62e94c8 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1097,6 +1097,9 @@ "kept_this_deleted_others": "Kept this asset and deleted {count, plural, one {# asset} other {# assets}}", "keyboard_shortcuts": "Keyboard shortcuts", "language": "Language", + "language_no_results_subtitle": "Try adjusting your search term", + "language_no_results_title": "No languages found", + "language_search_hint": "Search languages...", "language_setting_description": "Select your preferred language", "last_seen": "Last seen", "latest_version": "Latest Version", @@ -1628,7 +1631,6 @@ "setting_image_viewer_title": "Images", "setting_languages_apply": "Apply", "setting_languages_subtitle": "Change the app's language", - "setting_languages_title": "Languages", "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {duration}", "setting_notifications_notify_hours": "{count} hours", "setting_notifications_notify_immediately": "immediately", diff --git a/mobile/lib/pages/common/settings.page.dart b/mobile/lib/pages/common/settings.page.dart index dc186720f31..05c76069708 100644 --- a/mobile/lib/pages/common/settings.page.dart +++ b/mobile/lib/pages/common/settings.page.dart @@ -30,7 +30,7 @@ enum SettingSection { "backup_setting_subtitle", ), languages( - 'setting_languages_title', + 'language', Icons.language, "setting_languages_subtitle", ), diff --git a/mobile/lib/widgets/settings/language_settings.dart b/mobile/lib/widgets/settings/language_settings.dart index 990dcfdfe8a..7dc7f89ea11 100644 --- a/mobile/lib/widgets/settings/language_settings.dart +++ b/mobile/lib/widgets/settings/language_settings.dart @@ -1,79 +1,324 @@ -import 'package:easy_localization/easy_localization.dart'; +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:immich_mobile/constants/locales.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/services/localization.service.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/widgets/common/search_field.dart'; class LanguageSettings extends HookConsumerWidget { const LanguageSettings({super.key}); + Future _applyLanguageChange( + BuildContext context, + ValueNotifier selectedLocale, + ValueNotifier isLoading, + ) async { + isLoading.value = true; + await Future.delayed(const Duration(milliseconds: 500)); + try { + await context.setLocale(selectedLocale.value); + await loadTranslations(); + } finally { + isLoading.value = false; + } + } + @override Widget build(BuildContext context, WidgetRef ref) { + final localeEntries = useMemoized(() => locales.entries.toList(), const []); final currentLocale = context.locale; - final textController = useTextEditingController( - text: locales.keys.firstWhere( - (countryName) => locales[countryName] == currentLocale, - ), - ); - + final filteredLocaleEntries = + useState>>(localeEntries); final selectedLocale = useState(currentLocale); - return ListView( - padding: const EdgeInsets.all(16), - children: [ - LayoutBuilder( - builder: (context, constraints) { - return DropdownMenu( - width: constraints.maxWidth, - inputDecorationTheme: InputDecorationTheme( - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(20), - ), - contentPadding: const EdgeInsets.only(left: 16), - ), - menuStyle: MenuStyle( - shape: WidgetStatePropertyAll( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), + final isLoading = useState(false); + final isButtonDisabled = + selectedLocale.value == currentLocale || isLoading.value; + + final searchController = useTextEditingController(); + final searchFocusNode = useFocusNode(); + final debounceTimer = useRef(null); + + void onSearch(String searchTerm) { + debounceTimer.value?.cancel(); + debounceTimer.value = Timer(const Duration(milliseconds: 500), () { + if (searchTerm.isEmpty) { + filteredLocaleEntries.value = localeEntries; + } else { + filteredLocaleEntries.value = localeEntries + .where( + (entry) => + entry.key.toLowerCase().contains(searchTerm.toLowerCase()), + ) + .toList(); + } + }); + } + + void clearSearch() { + searchController.clear(); + onSearch(''); + } + + useEffect( + () { + void searchListener() => onSearch(searchController.text); + searchController.addListener(searchListener); + return () { + searchController.removeListener(searchListener); + debounceTimer.value?.cancel(); + }; + }, + [searchController], + ); + + return SafeArea( + child: Column( + children: [ + _LanguageSearchBar( + controller: searchController, + focusNode: searchFocusNode, + onClear: clearSearch, + onChanged: (_) => onSearch(searchController.text), + ), + Expanded( + child: filteredLocaleEntries.value.isEmpty + ? const _LanguageNotFound() + : ListView.builder( + padding: const EdgeInsets.all(8), + itemCount: filteredLocaleEntries.value.length, + itemExtent: 64.0, + itemBuilder: (context, index) { + final countryName = + filteredLocaleEntries.value[index].key; + final localeValue = + filteredLocaleEntries.value[index].value; + final bool isSelected = + selectedLocale.value == localeValue; + + return _LanguageItem( + countryName: countryName, + localeValue: localeValue, + isSelected: isSelected, + onTap: () { + selectedLocale.value = localeValue; + }, + ); + }, ), - ), - backgroundColor: WidgetStatePropertyAll( - context.colorScheme.surfaceContainer, - ), + ), + if (filteredLocaleEntries.value.isNotEmpty) + _LanguageApplyButton( + isDisabled: isButtonDisabled, + isLoading: isLoading.value, + onPressed: () => _applyLanguageChange( + context, + selectedLocale, + isLoading, ), - menuHeight: context.height * 0.5, - hintText: "Languages", - label: const Text('Languages'), - dropdownMenuEntries: locales.keys - .map( - (countryName) => DropdownMenuEntry( - value: locales[countryName], - label: countryName, - ), - ) - .toList(), - controller: textController, - onSelected: (value) { - if (value != null) { - selectedLocale.value = value; - } - }, - ); - }, - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: selectedLocale.value == currentLocale - ? null - : () { - context.setLocale(selectedLocale.value); - loadTranslations(); - }, - child: const Text('setting_languages_apply').tr(), - ), - ], + ), + ], + ), + ); + } +} + +class _LanguageSearchBar extends StatelessWidget { + const _LanguageSearchBar({ + required this.controller, + required this.focusNode, + required this.onClear, + required this.onChanged, + }); + + final TextEditingController controller; + final FocusNode focusNode; + final VoidCallback onClear; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.only(top: 16, bottom: 8, left: 50, right: 50), + decoration: BoxDecoration( + color: context.colorScheme.surface, + ), + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(24)), + gradient: LinearGradient( + colors: [ + context.colorScheme.primary.withValues(alpha: 0.075), + context.colorScheme.primary.withValues(alpha: 0.09), + context.colorScheme.primary.withValues(alpha: 0.075), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + child: SearchField( + autofocus: false, + contentPadding: const EdgeInsets.all(12), + hintText: 'language_search_hint'.tr(), + prefixIcon: const Icon(Icons.search_rounded), + suffixIcon: controller.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear_rounded), + onPressed: onClear, + ) + : null, + controller: controller, + onChanged: onChanged, + focusNode: focusNode, + onTapOutside: (_) => focusNode.unfocus(), + ), + ), + ); + } +} + +class _LanguageNotFound extends StatelessWidget { + const _LanguageNotFound(); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.search_off_rounded, + size: 64, + color: context.colorScheme.onSurface.withValues(alpha: 0.4), + ), + const SizedBox(height: 8), + Text( + 'language_no_results_title'.tr(), + style: context.textTheme.titleMedium?.copyWith( + color: context.colorScheme.onSurface, + ), + ), + const SizedBox(height: 4), + Text( + 'language_no_results_subtitle'.tr(), + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurface.withValues(alpha: 0.8), + ), + ), + ], + ), + ); + } +} + +class _LanguageApplyButton extends StatelessWidget { + const _LanguageApplyButton({ + required this.isDisabled, + required this.isLoading, + required this.onPressed, + }); + + final bool isDisabled; + final bool isLoading; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + color: context.colorScheme.surface, + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: SizedBox( + width: double.infinity, + height: 48, + child: ElevatedButton( + onPressed: isDisabled ? null : onPressed, + child: isLoading + ? const SizedBox.square( + dimension: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : Text( + 'setting_languages_apply'.tr(), + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16.0, + ), + ), + ), + ), + ), + ); + } +} + +class _LanguageItem extends StatelessWidget { + const _LanguageItem({ + required this.countryName, + required this.localeValue, + required this.isSelected, + required this.onTap, + }); + + final String countryName; + final Locale localeValue; + final bool isSelected; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 4.0, + horizontal: 8.0, + ), + child: DecoratedBox( + decoration: BoxDecoration( + color: + context.colorScheme.surfaceContainerLowest.withValues(alpha: .6), + borderRadius: const BorderRadius.all( + Radius.circular(16.0), + ), + border: Border.all( + color: context.colorScheme.outlineVariant.withValues(alpha: .4), + width: 1.0, + ), + ), + child: ListTile( + title: Text( + countryName, + style: context.textTheme.titleSmall?.copyWith( + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + color: isSelected + ? context.colorScheme.primary + : context.colorScheme.onSurfaceVariant, + ), + ), + trailing: isSelected + ? Icon( + Icons.check, + color: context.colorScheme.primary, + size: 20, + ) + : null, + onTap: onTap, + selected: isSelected, + selectedTileColor: context.colorScheme.primary.withValues(alpha: .15), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16.0)), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16.0, + ), + ), + ), ); } } From e4322ae0a2124092009b63cc1a5718b122db503b Mon Sep 17 00:00:00 2001 From: Thien Dang <89862334+dvbthien@users.noreply.github.com> Date: Tue, 3 Jun 2025 21:33:13 +0700 Subject: [PATCH 47/50] feat(mobile): Add new language to mobile (#18891) add pt_BR, bg, ta, te locates Co-authored-by: dvbthien --- mobile/lib/constants/locales.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mobile/lib/constants/locales.dart b/mobile/lib/constants/locales.dart index b4d4b63660c..658242ea3ad 100644 --- a/mobile/lib/constants/locales.dart +++ b/mobile/lib/constants/locales.dart @@ -5,6 +5,7 @@ const Map locales = { 'English (en)': Locale('en'), // Additional locales 'Arabic (ar)': Locale('ar'), + 'Bulgarian (bg)': Locale('bg'), 'Catalan (ca)': Locale('ca'), 'Chinese Simplified (zh_CN)': Locale.fromSubtags(languageCode: 'zh', scriptCode: 'SIMPLIFIED'), @@ -31,6 +32,7 @@ const Map locales = { 'Mongolian (mn)': Locale('mn'), 'Norwegian Bokmål (nb_NO)': Locale('nb', 'NO'), 'Polish (pl)': Locale('pl'), + 'Brazilian Portuguese (pt_BR)': Locale('pt', 'BR'), 'Portuguese (pt)': Locale('pt'), 'Romanian (ro)': Locale('ro'), 'Russian (ru)': Locale('ru'), @@ -42,6 +44,8 @@ const Map locales = { 'Slovenian (sl)': Locale('sl'), 'Spanish (es)': Locale('es'), 'Swedish (sv)': Locale('sv'), + 'Tamil (ta)': Locale('ta'), + 'Telugu (te)': Locale('te'), 'Thai (th)': Locale('th'), 'Turkish (tr)': Locale('tr'), 'Ukrainian (uk)': Locale('uk'), From 246d593c9d044b17aed80509ab96493035a8f102 Mon Sep 17 00:00:00 2001 From: JobiJoba <6494791+JobiJoba@users.noreply.github.com> Date: Tue, 3 Jun 2025 21:36:14 +0700 Subject: [PATCH 48/50] fix(mobile): reset current asset if we try to go on a activity list page (#18895) --- mobile/lib/pages/album/album_viewer.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mobile/lib/pages/album/album_viewer.dart b/mobile/lib/pages/album/album_viewer.dart index 75b2c09af3b..f6c46843db2 100644 --- a/mobile/lib/pages/album/album_viewer.dart +++ b/mobile/lib/pages/album/album_viewer.dart @@ -14,6 +14,7 @@ import 'package:immich_mobile/pages/album/album_shared_user_icons.dart'; import 'package:immich_mobile/pages/album/album_title.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/album/current_album.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/timeline.provider.dart'; import 'package:immich_mobile/utils/immich_loading_overlay.dart'; import 'package:immich_mobile/providers/multiselect.provider.dart'; @@ -93,6 +94,7 @@ class AlbumViewer extends HookConsumerWidget { onActivitiesPressed() { if (album.remoteId != null) { + ref.read(currentAssetProvider.notifier).set(null); context.pushRoute( const ActivitiesRoute(), ); From edae9c2d3d972b3a7ad1a4f73d9f572e42f4de85 Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Tue, 3 Jun 2025 16:52:29 +0200 Subject: [PATCH 49/50] fix(mobile): t function localization (#18768) * fix(mobile): items translation * Intl.defaultLocale null coalescence --- mobile/lib/utils/translation.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mobile/lib/utils/translation.dart b/mobile/lib/utils/translation.dart index 461e88ead70..1a33161dbcd 100644 --- a/mobile/lib/utils/translation.dart +++ b/mobile/lib/utils/translation.dart @@ -5,7 +5,8 @@ String t(String key, [Map? args]) { try { String message = key.tr(); if (args != null) { - return MessageFormat(message).format(args); + return MessageFormat(message, locale: Intl.defaultLocale ?? 'en') + .format(args); } return message; } catch (e) { From b4a798c39fa8bce75f560663d618a9d568e01ff3 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Tue, 3 Jun 2025 21:31:50 +0530 Subject: [PATCH 50/50] feat(mobile): remote asset & exif sync (#18756) * feat(mobile): remote asset & exif sync * add visibility and update constraints * chore: generate drifts * update ids to be strings * clear remote entities on logout * reset sqlite button --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../drift_schemas/main/drift_schema_v1.json | 2 +- .../lib/domain/models/asset/asset.model.dart | 18 +- .../domain/services/sync_stream.service.dart | 1 - mobile/lib/extensions/string_extensions.dart | 9 - .../infrastructure/entities/exif.entity.dart | 53 + .../entities/exif.entity.drift.dart | 1484 +++++++++++++++++ .../entities/local_asset.entity.dart | 2 +- .../entities/local_asset.entity.drift.dart | 4 +- .../entities/partner.entity.dart | 8 +- .../entities/partner.entity.drift.dart | 228 ++- .../entities/remote_asset.entity.dart | 35 + .../entities/remote_asset.entity.drift.dart | 1076 ++++++++++++ .../infrastructure/entities/user.entity.dart | 2 +- .../entities/user.entity.drift.dart | 61 +- .../entities/user_metadata.entity.dart | 4 +- .../entities/user_metadata.entity.drift.dart | 149 +- .../repositories/db.repository.dart | 4 + .../repositories/db.repository.drift.dart | 31 +- .../repositories/sync_api.repository.dart | 2 + .../repositories/sync_stream.repository.dart | 164 +- .../pages/dev/feat_in_development.page.dart | 27 + ...ia_stat.page.dart => media_stat.page.dart} | 98 +- mobile/lib/repositories/auth.repository.dart | 12 +- mobile/lib/routing/router.dart | 6 +- mobile/lib/routing/router.gr.dart | 16 + mobile/makefile | 2 +- 26 files changed, 3196 insertions(+), 302 deletions(-) create mode 100644 mobile/lib/infrastructure/entities/exif.entity.drift.dart create mode 100644 mobile/lib/infrastructure/entities/remote_asset.entity.dart create mode 100644 mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart rename mobile/lib/presentation/pages/dev/{local_media_stat.page.dart => media_stat.page.dart} (76%) diff --git a/mobile/drift_schemas/main/drift_schema_v1.json b/mobile/drift_schemas/main/drift_schema_v1.json index 1870ef477f9..5cdec3d9245 100644 --- a/mobile/drift_schemas/main/drift_schema_v1.json +++ b/mobile/drift_schemas/main/drift_schema_v1.json @@ -1 +1 @@ -{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"profile_image_path","getter_name":"profileImagePath","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"user_metadata_entity","was_declared_in_moor":false,"columns":[{"name":"user_id","getter_name":"userId","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"preferences","getter_name":"preferences","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userPreferenceConverter","dart_type_name":"UserPreferences"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id"]}},{"id":2,"references":[0],"type":"table","data":{"name":"partner_entity","was_declared_in_moor":false,"columns":[{"name":"shared_by_id","getter_name":"sharedById","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"shared_with_id","getter_name":"sharedWithId","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"in_timeline","getter_name":"inTimeline","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"in_timeline\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"in_timeline\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["shared_by_id","shared_with_id"]}}]} \ No newline at end of file +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"profile_image_path","getter_name":"profileImagePath","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"user_metadata_entity","was_declared_in_moor":false,"columns":[{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"preferences","getter_name":"preferences","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userPreferenceConverter","dart_type_name":"UserPreferences"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id"]}},{"id":2,"references":[0],"type":"table","data":{"name":"partner_entity","was_declared_in_moor":false,"columns":[{"name":"shared_by_id","getter_name":"sharedById","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"shared_with_id","getter_name":"sharedWithId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"in_timeline","getter_name":"inTimeline","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"in_timeline\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"in_timeline\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["shared_by_id","shared_with_id"]}},{"id":3,"references":[],"type":"table","data":{"name":"local_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"backup_selection","getter_name":"backupSelection","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(BackupSelection.values)","dart_type_name":"BackupSelection"}},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":4,"references":[],"type":"table","data":{"name":"local_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":5,"references":[4,3],"type":"table","data":{"name":"local_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":6,"references":[0],"type":"table","data":{"name":"remote_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"local_date_time","getter_name":"localDateTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumb_hash","getter_name":"thumbHash","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"visibility","getter_name":"visibility","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetVisibility.values)","dart_type_name":"AssetVisibility"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":7,"references":[6],"type":"table","data":{"name":"remote_exif_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"city","getter_name":"city","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"state","getter_name":"state","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"country","getter_name":"country","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"date_time_original","getter_name":"dateTimeOriginal","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"exposure_time","getter_name":"exposureTime","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"f_number","getter_name":"fNumber","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"file_size","getter_name":"fileSize","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"focal_length","getter_name":"focalLength","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"latitude","getter_name":"latitude","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"longitude","getter_name":"longitude","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"iso","getter_name":"iso","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"make","getter_name":"make","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"model","getter_name":"model","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"time_zone","getter_name":"timeZone","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"rating","getter_name":"rating","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"projection_type","getter_name":"projectionType","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id"]}},{"id":8,"references":[4],"type":"index","data":{"on":4,"name":"idx_local_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":9,"references":[6],"type":"index","data":{"on":6,"name":"UQ_remote_asset_owner_checksum","sql":null,"unique":true,"columns":["checksum","owner_id"]}}]} \ No newline at end of file diff --git a/mobile/lib/domain/models/asset/asset.model.dart b/mobile/lib/domain/models/asset/asset.model.dart index e2bb1fc49f6..c170f7f8485 100644 --- a/mobile/lib/domain/models/asset/asset.model.dart +++ b/mobile/lib/domain/models/asset/asset.model.dart @@ -1,9 +1,17 @@ part of 'base_asset.model.dart'; +enum AssetVisibility { + timeline, + hidden, + archive, + locked, +} + // Model for an asset stored in the server class Asset extends BaseAsset { final String id; final String? localId; + final AssetVisibility visibility; const Asset({ required this.id, @@ -17,6 +25,7 @@ class Asset extends BaseAsset { super.height, super.durationInSeconds, super.isFavorite = false, + this.visibility = AssetVisibility.timeline, }); @override @@ -32,6 +41,7 @@ class Asset extends BaseAsset { durationInSeconds: ${durationInSeconds ?? ""}, localId: ${localId ?? ""}, isFavorite: $isFavorite, + visibility: $visibility, }'''; } @@ -39,9 +49,13 @@ class Asset extends BaseAsset { bool operator ==(Object other) { if (other is! Asset) return false; if (identical(this, other)) return true; - return super == other && id == other.id && localId == other.localId; + return super == other && + id == other.id && + localId == other.localId && + visibility == other.visibility; } @override - int get hashCode => super.hashCode ^ id.hashCode ^ localId.hashCode; + int get hashCode => + super.hashCode ^ id.hashCode ^ localId.hashCode ^ visibility.hashCode; } diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart index ac63734b07d..00f97825b23 100644 --- a/mobile/lib/domain/services/sync_stream.service.dart +++ b/mobile/lib/domain/services/sync_stream.service.dart @@ -63,7 +63,6 @@ class SyncStreamService { Iterable data, ) async { _logger.fine("Processing sync data for $type of length ${data.length}"); - // ignore: prefer-switch-expression switch (type) { case SyncEntityType.userV1: return _syncStreamRepository.updateUsersV1(data.cast()); diff --git a/mobile/lib/extensions/string_extensions.dart b/mobile/lib/extensions/string_extensions.dart index 73c8c2d34c3..67411013eed 100644 --- a/mobile/lib/extensions/string_extensions.dart +++ b/mobile/lib/extensions/string_extensions.dart @@ -1,7 +1,3 @@ -import 'dart:typed_data'; - -import 'package:uuid/parsing.dart'; - extension StringExtension on String { String capitalize() { return split(" ") @@ -33,8 +29,3 @@ extension DurationExtension on String { return int.parse(this); } } - -extension UUIDExtension on String { - Uint8List toUuidByte({bool shouldValidate = false}) => - UuidParsing.parseAsByteList(this, validate: shouldValidate); -} diff --git a/mobile/lib/infrastructure/entities/exif.entity.dart b/mobile/lib/infrastructure/entities/exif.entity.dart index 5a93bc97681..11730b77615 100644 --- a/mobile/lib/infrastructure/entities/exif.entity.dart +++ b/mobile/lib/infrastructure/entities/exif.entity.dart @@ -1,4 +1,7 @@ +import 'package:drift/drift.dart' hide Query; import 'package:immich_mobile/domain/models/exif.model.dart' as domain; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; import 'package:isar/isar.dart'; @@ -90,3 +93,53 @@ class ExifInfo { exposureSeconds: exposureSeconds, ); } + +class RemoteExifEntity extends Table with DriftDefaultsMixin { + const RemoteExifEntity(); + + TextColumn get assetId => + text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)(); + + TextColumn get city => text().nullable()(); + + TextColumn get state => text().nullable()(); + + TextColumn get country => text().nullable()(); + + DateTimeColumn get dateTimeOriginal => dateTime().nullable()(); + + TextColumn get description => text().nullable()(); + + IntColumn get height => integer().nullable()(); + + IntColumn get width => integer().nullable()(); + + TextColumn get exposureTime => text().nullable()(); + + IntColumn get fNumber => integer().nullable()(); + + IntColumn get fileSize => integer().nullable()(); + + IntColumn get focalLength => integer().nullable()(); + + IntColumn get latitude => integer().nullable()(); + + IntColumn get longitude => integer().nullable()(); + + IntColumn get iso => integer().nullable()(); + + TextColumn get make => text().nullable()(); + + TextColumn get model => text().nullable()(); + + TextColumn get orientation => text().nullable()(); + + TextColumn get timeZone => text().nullable()(); + + IntColumn get rating => integer().nullable()(); + + TextColumn get projectionType => text().nullable()(); + + @override + Set get primaryKey => {assetId}; +} diff --git a/mobile/lib/infrastructure/entities/exif.entity.drift.dart b/mobile/lib/infrastructure/entities/exif.entity.drift.dart new file mode 100644 index 00000000000..10025d9cb87 --- /dev/null +++ b/mobile/lib/infrastructure/entities/exif.entity.drift.dart @@ -0,0 +1,1484 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart' + as i1; +import 'package:immich_mobile/infrastructure/entities/exif.entity.dart' as i2; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart' + as i3; +import 'package:drift/internal/modular.dart' as i4; + +typedef $$RemoteExifEntityTableCreateCompanionBuilder + = i1.RemoteExifEntityCompanion Function({ + required String assetId, + i0.Value city, + i0.Value state, + i0.Value country, + i0.Value dateTimeOriginal, + i0.Value description, + i0.Value height, + i0.Value width, + i0.Value exposureTime, + i0.Value fNumber, + i0.Value fileSize, + i0.Value focalLength, + i0.Value latitude, + i0.Value longitude, + i0.Value iso, + i0.Value make, + i0.Value model, + i0.Value orientation, + i0.Value timeZone, + i0.Value rating, + i0.Value projectionType, +}); +typedef $$RemoteExifEntityTableUpdateCompanionBuilder + = i1.RemoteExifEntityCompanion Function({ + i0.Value assetId, + i0.Value city, + i0.Value state, + i0.Value country, + i0.Value dateTimeOriginal, + i0.Value description, + i0.Value height, + i0.Value width, + i0.Value exposureTime, + i0.Value fNumber, + i0.Value fileSize, + i0.Value focalLength, + i0.Value latitude, + i0.Value longitude, + i0.Value iso, + i0.Value make, + i0.Value model, + i0.Value orientation, + i0.Value timeZone, + i0.Value rating, + i0.Value projectionType, +}); + +final class $$RemoteExifEntityTableReferences extends i0.BaseReferences< + i0.GeneratedDatabase, i1.$RemoteExifEntityTable, i1.RemoteExifEntityData> { + $$RemoteExifEntityTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static i3.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) => + i4.ReadDatabaseContainer(db) + .resultSet('remote_asset_entity') + .createAlias(i0.$_aliasNameGenerator( + i4.ReadDatabaseContainer(db) + .resultSet('remote_exif_entity') + .assetId, + i4.ReadDatabaseContainer(db) + .resultSet('remote_asset_entity') + .id)); + + i3.$$RemoteAssetEntityTableProcessedTableManager get assetId { + final $_column = $_itemColumn('asset_id')!; + + final manager = i3 + .$$RemoteAssetEntityTableTableManager( + $_db, + i4.ReadDatabaseContainer($_db) + .resultSet('remote_asset_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_assetIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$RemoteExifEntityTableFilterComposer + extends i0.Composer { + $$RemoteExifEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnFilters get city => $composableBuilder( + column: $table.city, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get state => $composableBuilder( + column: $table.state, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get country => $composableBuilder( + column: $table.country, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get dateTimeOriginal => $composableBuilder( + column: $table.dateTimeOriginal, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get description => $composableBuilder( + column: $table.description, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get height => $composableBuilder( + column: $table.height, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get width => $composableBuilder( + column: $table.width, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get exposureTime => $composableBuilder( + column: $table.exposureTime, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get fNumber => $composableBuilder( + column: $table.fNumber, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get fileSize => $composableBuilder( + column: $table.fileSize, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get focalLength => $composableBuilder( + column: $table.focalLength, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get latitude => $composableBuilder( + column: $table.latitude, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get longitude => $composableBuilder( + column: $table.longitude, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get iso => $composableBuilder( + column: $table.iso, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get make => $composableBuilder( + column: $table.make, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get model => $composableBuilder( + column: $table.model, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get orientation => $composableBuilder( + column: $table.orientation, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get timeZone => $composableBuilder( + column: $table.timeZone, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get rating => $composableBuilder( + column: $table.rating, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get projectionType => $composableBuilder( + column: $table.projectionType, + builder: (column) => i0.ColumnFilters(column)); + + i3.$$RemoteAssetEntityTableFilterComposer get assetId { + final i3.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$RemoteAssetEntityTableFilterComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteExifEntityTableOrderingComposer + extends i0.Composer { + $$RemoteExifEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get city => $composableBuilder( + column: $table.city, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get state => $composableBuilder( + column: $table.state, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get country => $composableBuilder( + column: $table.country, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get dateTimeOriginal => $composableBuilder( + column: $table.dateTimeOriginal, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get description => $composableBuilder( + column: $table.description, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get height => $composableBuilder( + column: $table.height, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get width => $composableBuilder( + column: $table.width, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get exposureTime => $composableBuilder( + column: $table.exposureTime, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get fNumber => $composableBuilder( + column: $table.fNumber, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get fileSize => $composableBuilder( + column: $table.fileSize, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get focalLength => $composableBuilder( + column: $table.focalLength, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get latitude => $composableBuilder( + column: $table.latitude, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get longitude => $composableBuilder( + column: $table.longitude, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get iso => $composableBuilder( + column: $table.iso, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get make => $composableBuilder( + column: $table.make, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get model => $composableBuilder( + column: $table.model, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get orientation => $composableBuilder( + column: $table.orientation, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get timeZone => $composableBuilder( + column: $table.timeZone, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get rating => $composableBuilder( + column: $table.rating, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get projectionType => $composableBuilder( + column: $table.projectionType, + builder: (column) => i0.ColumnOrderings(column)); + + i3.$$RemoteAssetEntityTableOrderingComposer get assetId { + final i3.$$RemoteAssetEntityTableOrderingComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$RemoteAssetEntityTableOrderingComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteExifEntityTableAnnotationComposer + extends i0.Composer { + $$RemoteExifEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumn get city => + $composableBuilder(column: $table.city, builder: (column) => column); + + i0.GeneratedColumn get state => + $composableBuilder(column: $table.state, builder: (column) => column); + + i0.GeneratedColumn get country => + $composableBuilder(column: $table.country, builder: (column) => column); + + i0.GeneratedColumn get dateTimeOriginal => $composableBuilder( + column: $table.dateTimeOriginal, builder: (column) => column); + + i0.GeneratedColumn get description => $composableBuilder( + column: $table.description, builder: (column) => column); + + i0.GeneratedColumn get height => + $composableBuilder(column: $table.height, builder: (column) => column); + + i0.GeneratedColumn get width => + $composableBuilder(column: $table.width, builder: (column) => column); + + i0.GeneratedColumn get exposureTime => $composableBuilder( + column: $table.exposureTime, builder: (column) => column); + + i0.GeneratedColumn get fNumber => + $composableBuilder(column: $table.fNumber, builder: (column) => column); + + i0.GeneratedColumn get fileSize => + $composableBuilder(column: $table.fileSize, builder: (column) => column); + + i0.GeneratedColumn get focalLength => $composableBuilder( + column: $table.focalLength, builder: (column) => column); + + i0.GeneratedColumn get latitude => + $composableBuilder(column: $table.latitude, builder: (column) => column); + + i0.GeneratedColumn get longitude => + $composableBuilder(column: $table.longitude, builder: (column) => column); + + i0.GeneratedColumn get iso => + $composableBuilder(column: $table.iso, builder: (column) => column); + + i0.GeneratedColumn get make => + $composableBuilder(column: $table.make, builder: (column) => column); + + i0.GeneratedColumn get model => + $composableBuilder(column: $table.model, builder: (column) => column); + + i0.GeneratedColumn get orientation => $composableBuilder( + column: $table.orientation, builder: (column) => column); + + i0.GeneratedColumn get timeZone => + $composableBuilder(column: $table.timeZone, builder: (column) => column); + + i0.GeneratedColumn get rating => + $composableBuilder(column: $table.rating, builder: (column) => column); + + i0.GeneratedColumn get projectionType => $composableBuilder( + column: $table.projectionType, builder: (column) => column); + + i3.$$RemoteAssetEntityTableAnnotationComposer get assetId { + final i3.$$RemoteAssetEntityTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$RemoteAssetEntityTableAnnotationComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteExifEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$RemoteExifEntityTable, + i1.RemoteExifEntityData, + i1.$$RemoteExifEntityTableFilterComposer, + i1.$$RemoteExifEntityTableOrderingComposer, + i1.$$RemoteExifEntityTableAnnotationComposer, + $$RemoteExifEntityTableCreateCompanionBuilder, + $$RemoteExifEntityTableUpdateCompanionBuilder, + (i1.RemoteExifEntityData, i1.$$RemoteExifEntityTableReferences), + i1.RemoteExifEntityData, + i0.PrefetchHooks Function({bool assetId})> { + $$RemoteExifEntityTableTableManager( + i0.GeneratedDatabase db, i1.$RemoteExifEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$RemoteExifEntityTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => i1 + .$$RemoteExifEntityTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + i1.$$RemoteExifEntityTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + i0.Value assetId = const i0.Value.absent(), + i0.Value city = const i0.Value.absent(), + i0.Value state = const i0.Value.absent(), + i0.Value country = const i0.Value.absent(), + i0.Value dateTimeOriginal = const i0.Value.absent(), + i0.Value description = const i0.Value.absent(), + i0.Value height = const i0.Value.absent(), + i0.Value width = const i0.Value.absent(), + i0.Value exposureTime = const i0.Value.absent(), + i0.Value fNumber = const i0.Value.absent(), + i0.Value fileSize = const i0.Value.absent(), + i0.Value focalLength = const i0.Value.absent(), + i0.Value latitude = const i0.Value.absent(), + i0.Value longitude = const i0.Value.absent(), + i0.Value iso = const i0.Value.absent(), + i0.Value make = const i0.Value.absent(), + i0.Value model = const i0.Value.absent(), + i0.Value orientation = const i0.Value.absent(), + i0.Value timeZone = const i0.Value.absent(), + i0.Value rating = const i0.Value.absent(), + i0.Value projectionType = const i0.Value.absent(), + }) => + i1.RemoteExifEntityCompanion( + assetId: assetId, + city: city, + state: state, + country: country, + dateTimeOriginal: dateTimeOriginal, + description: description, + height: height, + width: width, + exposureTime: exposureTime, + fNumber: fNumber, + fileSize: fileSize, + focalLength: focalLength, + latitude: latitude, + longitude: longitude, + iso: iso, + make: make, + model: model, + orientation: orientation, + timeZone: timeZone, + rating: rating, + projectionType: projectionType, + ), + createCompanionCallback: ({ + required String assetId, + i0.Value city = const i0.Value.absent(), + i0.Value state = const i0.Value.absent(), + i0.Value country = const i0.Value.absent(), + i0.Value dateTimeOriginal = const i0.Value.absent(), + i0.Value description = const i0.Value.absent(), + i0.Value height = const i0.Value.absent(), + i0.Value width = const i0.Value.absent(), + i0.Value exposureTime = const i0.Value.absent(), + i0.Value fNumber = const i0.Value.absent(), + i0.Value fileSize = const i0.Value.absent(), + i0.Value focalLength = const i0.Value.absent(), + i0.Value latitude = const i0.Value.absent(), + i0.Value longitude = const i0.Value.absent(), + i0.Value iso = const i0.Value.absent(), + i0.Value make = const i0.Value.absent(), + i0.Value model = const i0.Value.absent(), + i0.Value orientation = const i0.Value.absent(), + i0.Value timeZone = const i0.Value.absent(), + i0.Value rating = const i0.Value.absent(), + i0.Value projectionType = const i0.Value.absent(), + }) => + i1.RemoteExifEntityCompanion.insert( + assetId: assetId, + city: city, + state: state, + country: country, + dateTimeOriginal: dateTimeOriginal, + description: description, + height: height, + width: width, + exposureTime: exposureTime, + fNumber: fNumber, + fileSize: fileSize, + focalLength: focalLength, + latitude: latitude, + longitude: longitude, + iso: iso, + make: make, + model: model, + orientation: orientation, + timeZone: timeZone, + rating: rating, + projectionType: projectionType, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + i1.$$RemoteExifEntityTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({assetId = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends i0.TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (assetId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.assetId, + referencedTable: + i1.$$RemoteExifEntityTableReferences._assetIdTable(db), + referencedColumn: i1.$$RemoteExifEntityTableReferences + ._assetIdTable(db) + .id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$RemoteExifEntityTableProcessedTableManager = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$RemoteExifEntityTable, + i1.RemoteExifEntityData, + i1.$$RemoteExifEntityTableFilterComposer, + i1.$$RemoteExifEntityTableOrderingComposer, + i1.$$RemoteExifEntityTableAnnotationComposer, + $$RemoteExifEntityTableCreateCompanionBuilder, + $$RemoteExifEntityTableUpdateCompanionBuilder, + (i1.RemoteExifEntityData, i1.$$RemoteExifEntityTableReferences), + i1.RemoteExifEntityData, + i0.PrefetchHooks Function({bool assetId})>; + +class $RemoteExifEntityTable extends i2.RemoteExifEntity + with i0.TableInfo<$RemoteExifEntityTable, i1.RemoteExifEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $RemoteExifEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _assetIdMeta = + const i0.VerificationMeta('assetId'); + @override + late final i0.GeneratedColumn assetId = i0.GeneratedColumn( + 'asset_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); + static const i0.VerificationMeta _cityMeta = + const i0.VerificationMeta('city'); + @override + late final i0.GeneratedColumn city = i0.GeneratedColumn( + 'city', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _stateMeta = + const i0.VerificationMeta('state'); + @override + late final i0.GeneratedColumn state = i0.GeneratedColumn( + 'state', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _countryMeta = + const i0.VerificationMeta('country'); + @override + late final i0.GeneratedColumn country = i0.GeneratedColumn( + 'country', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _dateTimeOriginalMeta = + const i0.VerificationMeta('dateTimeOriginal'); + @override + late final i0.GeneratedColumn dateTimeOriginal = + i0.GeneratedColumn('date_time_original', aliasedName, true, + type: i0.DriftSqlType.dateTime, requiredDuringInsert: false); + static const i0.VerificationMeta _descriptionMeta = + const i0.VerificationMeta('description'); + @override + late final i0.GeneratedColumn description = + i0.GeneratedColumn('description', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _heightMeta = + const i0.VerificationMeta('height'); + @override + late final i0.GeneratedColumn height = i0.GeneratedColumn( + 'height', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _widthMeta = + const i0.VerificationMeta('width'); + @override + late final i0.GeneratedColumn width = i0.GeneratedColumn( + 'width', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _exposureTimeMeta = + const i0.VerificationMeta('exposureTime'); + @override + late final i0.GeneratedColumn exposureTime = + i0.GeneratedColumn('exposure_time', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _fNumberMeta = + const i0.VerificationMeta('fNumber'); + @override + late final i0.GeneratedColumn fNumber = i0.GeneratedColumn( + 'f_number', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _fileSizeMeta = + const i0.VerificationMeta('fileSize'); + @override + late final i0.GeneratedColumn fileSize = i0.GeneratedColumn( + 'file_size', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _focalLengthMeta = + const i0.VerificationMeta('focalLength'); + @override + late final i0.GeneratedColumn focalLength = i0.GeneratedColumn( + 'focal_length', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _latitudeMeta = + const i0.VerificationMeta('latitude'); + @override + late final i0.GeneratedColumn latitude = i0.GeneratedColumn( + 'latitude', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _longitudeMeta = + const i0.VerificationMeta('longitude'); + @override + late final i0.GeneratedColumn longitude = i0.GeneratedColumn( + 'longitude', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _isoMeta = const i0.VerificationMeta('iso'); + @override + late final i0.GeneratedColumn iso = i0.GeneratedColumn( + 'iso', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _makeMeta = + const i0.VerificationMeta('make'); + @override + late final i0.GeneratedColumn make = i0.GeneratedColumn( + 'make', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _modelMeta = + const i0.VerificationMeta('model'); + @override + late final i0.GeneratedColumn model = i0.GeneratedColumn( + 'model', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _orientationMeta = + const i0.VerificationMeta('orientation'); + @override + late final i0.GeneratedColumn orientation = + i0.GeneratedColumn('orientation', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _timeZoneMeta = + const i0.VerificationMeta('timeZone'); + @override + late final i0.GeneratedColumn timeZone = i0.GeneratedColumn( + 'time_zone', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _ratingMeta = + const i0.VerificationMeta('rating'); + @override + late final i0.GeneratedColumn rating = i0.GeneratedColumn( + 'rating', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _projectionTypeMeta = + const i0.VerificationMeta('projectionType'); + @override + late final i0.GeneratedColumn projectionType = + i0.GeneratedColumn('projection_type', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + @override + List get $columns => [ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + orientation, + timeZone, + rating, + projectionType + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_exif_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('asset_id')) { + context.handle(_assetIdMeta, + assetId.isAcceptableOrUnknown(data['asset_id']!, _assetIdMeta)); + } else if (isInserting) { + context.missing(_assetIdMeta); + } + if (data.containsKey('city')) { + context.handle( + _cityMeta, city.isAcceptableOrUnknown(data['city']!, _cityMeta)); + } + if (data.containsKey('state')) { + context.handle( + _stateMeta, state.isAcceptableOrUnknown(data['state']!, _stateMeta)); + } + if (data.containsKey('country')) { + context.handle(_countryMeta, + country.isAcceptableOrUnknown(data['country']!, _countryMeta)); + } + if (data.containsKey('date_time_original')) { + context.handle( + _dateTimeOriginalMeta, + dateTimeOriginal.isAcceptableOrUnknown( + data['date_time_original']!, _dateTimeOriginalMeta)); + } + if (data.containsKey('description')) { + context.handle( + _descriptionMeta, + description.isAcceptableOrUnknown( + data['description']!, _descriptionMeta)); + } + if (data.containsKey('height')) { + context.handle(_heightMeta, + height.isAcceptableOrUnknown(data['height']!, _heightMeta)); + } + if (data.containsKey('width')) { + context.handle( + _widthMeta, width.isAcceptableOrUnknown(data['width']!, _widthMeta)); + } + if (data.containsKey('exposure_time')) { + context.handle( + _exposureTimeMeta, + exposureTime.isAcceptableOrUnknown( + data['exposure_time']!, _exposureTimeMeta)); + } + if (data.containsKey('f_number')) { + context.handle(_fNumberMeta, + fNumber.isAcceptableOrUnknown(data['f_number']!, _fNumberMeta)); + } + if (data.containsKey('file_size')) { + context.handle(_fileSizeMeta, + fileSize.isAcceptableOrUnknown(data['file_size']!, _fileSizeMeta)); + } + if (data.containsKey('focal_length')) { + context.handle( + _focalLengthMeta, + focalLength.isAcceptableOrUnknown( + data['focal_length']!, _focalLengthMeta)); + } + if (data.containsKey('latitude')) { + context.handle(_latitudeMeta, + latitude.isAcceptableOrUnknown(data['latitude']!, _latitudeMeta)); + } + if (data.containsKey('longitude')) { + context.handle(_longitudeMeta, + longitude.isAcceptableOrUnknown(data['longitude']!, _longitudeMeta)); + } + if (data.containsKey('iso')) { + context.handle( + _isoMeta, iso.isAcceptableOrUnknown(data['iso']!, _isoMeta)); + } + if (data.containsKey('make')) { + context.handle( + _makeMeta, make.isAcceptableOrUnknown(data['make']!, _makeMeta)); + } + if (data.containsKey('model')) { + context.handle( + _modelMeta, model.isAcceptableOrUnknown(data['model']!, _modelMeta)); + } + if (data.containsKey('orientation')) { + context.handle( + _orientationMeta, + orientation.isAcceptableOrUnknown( + data['orientation']!, _orientationMeta)); + } + if (data.containsKey('time_zone')) { + context.handle(_timeZoneMeta, + timeZone.isAcceptableOrUnknown(data['time_zone']!, _timeZoneMeta)); + } + if (data.containsKey('rating')) { + context.handle(_ratingMeta, + rating.isAcceptableOrUnknown(data['rating']!, _ratingMeta)); + } + if (data.containsKey('projection_type')) { + context.handle( + _projectionTypeMeta, + projectionType.isAcceptableOrUnknown( + data['projection_type']!, _projectionTypeMeta)); + } + return context; + } + + @override + Set get $primaryKey => {assetId}; + @override + i1.RemoteExifEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.RemoteExifEntityData( + assetId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + city: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}city']), + state: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}state']), + country: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}country']), + dateTimeOriginal: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, + data['${effectivePrefix}date_time_original']), + description: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}description']), + height: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}height']), + width: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}width']), + exposureTime: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, data['${effectivePrefix}exposure_time']), + fNumber: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}f_number']), + fileSize: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}file_size']), + focalLength: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}focal_length']), + latitude: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}latitude']), + longitude: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}longitude']), + iso: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}iso']), + make: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}make']), + model: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}model']), + orientation: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}orientation']), + timeZone: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}time_zone']), + rating: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}rating']), + projectionType: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, data['${effectivePrefix}projection_type']), + ); + } + + @override + $RemoteExifEntityTable createAlias(String alias) { + return $RemoteExifEntityTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteExifEntityData extends i0.DataClass + implements i0.Insertable { + final String assetId; + final String? city; + final String? state; + final String? country; + final DateTime? dateTimeOriginal; + final String? description; + final int? height; + final int? width; + final String? exposureTime; + final int? fNumber; + final int? fileSize; + final int? focalLength; + final int? latitude; + final int? longitude; + final int? iso; + final String? make; + final String? model; + final String? orientation; + final String? timeZone; + final int? rating; + final String? projectionType; + const RemoteExifEntityData( + {required this.assetId, + this.city, + this.state, + this.country, + this.dateTimeOriginal, + this.description, + this.height, + this.width, + this.exposureTime, + this.fNumber, + this.fileSize, + this.focalLength, + this.latitude, + this.longitude, + this.iso, + this.make, + this.model, + this.orientation, + this.timeZone, + this.rating, + this.projectionType}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = i0.Variable(assetId); + if (!nullToAbsent || city != null) { + map['city'] = i0.Variable(city); + } + if (!nullToAbsent || state != null) { + map['state'] = i0.Variable(state); + } + if (!nullToAbsent || country != null) { + map['country'] = i0.Variable(country); + } + if (!nullToAbsent || dateTimeOriginal != null) { + map['date_time_original'] = i0.Variable(dateTimeOriginal); + } + if (!nullToAbsent || description != null) { + map['description'] = i0.Variable(description); + } + if (!nullToAbsent || height != null) { + map['height'] = i0.Variable(height); + } + if (!nullToAbsent || width != null) { + map['width'] = i0.Variable(width); + } + if (!nullToAbsent || exposureTime != null) { + map['exposure_time'] = i0.Variable(exposureTime); + } + if (!nullToAbsent || fNumber != null) { + map['f_number'] = i0.Variable(fNumber); + } + if (!nullToAbsent || fileSize != null) { + map['file_size'] = i0.Variable(fileSize); + } + if (!nullToAbsent || focalLength != null) { + map['focal_length'] = i0.Variable(focalLength); + } + if (!nullToAbsent || latitude != null) { + map['latitude'] = i0.Variable(latitude); + } + if (!nullToAbsent || longitude != null) { + map['longitude'] = i0.Variable(longitude); + } + if (!nullToAbsent || iso != null) { + map['iso'] = i0.Variable(iso); + } + if (!nullToAbsent || make != null) { + map['make'] = i0.Variable(make); + } + if (!nullToAbsent || model != null) { + map['model'] = i0.Variable(model); + } + if (!nullToAbsent || orientation != null) { + map['orientation'] = i0.Variable(orientation); + } + if (!nullToAbsent || timeZone != null) { + map['time_zone'] = i0.Variable(timeZone); + } + if (!nullToAbsent || rating != null) { + map['rating'] = i0.Variable(rating); + } + if (!nullToAbsent || projectionType != null) { + map['projection_type'] = i0.Variable(projectionType); + } + return map; + } + + factory RemoteExifEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return RemoteExifEntityData( + assetId: serializer.fromJson(json['assetId']), + city: serializer.fromJson(json['city']), + state: serializer.fromJson(json['state']), + country: serializer.fromJson(json['country']), + dateTimeOriginal: + serializer.fromJson(json['dateTimeOriginal']), + description: serializer.fromJson(json['description']), + height: serializer.fromJson(json['height']), + width: serializer.fromJson(json['width']), + exposureTime: serializer.fromJson(json['exposureTime']), + fNumber: serializer.fromJson(json['fNumber']), + fileSize: serializer.fromJson(json['fileSize']), + focalLength: serializer.fromJson(json['focalLength']), + latitude: serializer.fromJson(json['latitude']), + longitude: serializer.fromJson(json['longitude']), + iso: serializer.fromJson(json['iso']), + make: serializer.fromJson(json['make']), + model: serializer.fromJson(json['model']), + orientation: serializer.fromJson(json['orientation']), + timeZone: serializer.fromJson(json['timeZone']), + rating: serializer.fromJson(json['rating']), + projectionType: serializer.fromJson(json['projectionType']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'city': serializer.toJson(city), + 'state': serializer.toJson(state), + 'country': serializer.toJson(country), + 'dateTimeOriginal': serializer.toJson(dateTimeOriginal), + 'description': serializer.toJson(description), + 'height': serializer.toJson(height), + 'width': serializer.toJson(width), + 'exposureTime': serializer.toJson(exposureTime), + 'fNumber': serializer.toJson(fNumber), + 'fileSize': serializer.toJson(fileSize), + 'focalLength': serializer.toJson(focalLength), + 'latitude': serializer.toJson(latitude), + 'longitude': serializer.toJson(longitude), + 'iso': serializer.toJson(iso), + 'make': serializer.toJson(make), + 'model': serializer.toJson(model), + 'orientation': serializer.toJson(orientation), + 'timeZone': serializer.toJson(timeZone), + 'rating': serializer.toJson(rating), + 'projectionType': serializer.toJson(projectionType), + }; + } + + i1.RemoteExifEntityData copyWith( + {String? assetId, + i0.Value city = const i0.Value.absent(), + i0.Value state = const i0.Value.absent(), + i0.Value country = const i0.Value.absent(), + i0.Value dateTimeOriginal = const i0.Value.absent(), + i0.Value description = const i0.Value.absent(), + i0.Value height = const i0.Value.absent(), + i0.Value width = const i0.Value.absent(), + i0.Value exposureTime = const i0.Value.absent(), + i0.Value fNumber = const i0.Value.absent(), + i0.Value fileSize = const i0.Value.absent(), + i0.Value focalLength = const i0.Value.absent(), + i0.Value latitude = const i0.Value.absent(), + i0.Value longitude = const i0.Value.absent(), + i0.Value iso = const i0.Value.absent(), + i0.Value make = const i0.Value.absent(), + i0.Value model = const i0.Value.absent(), + i0.Value orientation = const i0.Value.absent(), + i0.Value timeZone = const i0.Value.absent(), + i0.Value rating = const i0.Value.absent(), + i0.Value projectionType = const i0.Value.absent()}) => + i1.RemoteExifEntityData( + assetId: assetId ?? this.assetId, + city: city.present ? city.value : this.city, + state: state.present ? state.value : this.state, + country: country.present ? country.value : this.country, + dateTimeOriginal: dateTimeOriginal.present + ? dateTimeOriginal.value + : this.dateTimeOriginal, + description: description.present ? description.value : this.description, + height: height.present ? height.value : this.height, + width: width.present ? width.value : this.width, + exposureTime: + exposureTime.present ? exposureTime.value : this.exposureTime, + fNumber: fNumber.present ? fNumber.value : this.fNumber, + fileSize: fileSize.present ? fileSize.value : this.fileSize, + focalLength: focalLength.present ? focalLength.value : this.focalLength, + latitude: latitude.present ? latitude.value : this.latitude, + longitude: longitude.present ? longitude.value : this.longitude, + iso: iso.present ? iso.value : this.iso, + make: make.present ? make.value : this.make, + model: model.present ? model.value : this.model, + orientation: orientation.present ? orientation.value : this.orientation, + timeZone: timeZone.present ? timeZone.value : this.timeZone, + rating: rating.present ? rating.value : this.rating, + projectionType: + projectionType.present ? projectionType.value : this.projectionType, + ); + RemoteExifEntityData copyWithCompanion(i1.RemoteExifEntityCompanion data) { + return RemoteExifEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + city: data.city.present ? data.city.value : this.city, + state: data.state.present ? data.state.value : this.state, + country: data.country.present ? data.country.value : this.country, + dateTimeOriginal: data.dateTimeOriginal.present + ? data.dateTimeOriginal.value + : this.dateTimeOriginal, + description: + data.description.present ? data.description.value : this.description, + height: data.height.present ? data.height.value : this.height, + width: data.width.present ? data.width.value : this.width, + exposureTime: data.exposureTime.present + ? data.exposureTime.value + : this.exposureTime, + fNumber: data.fNumber.present ? data.fNumber.value : this.fNumber, + fileSize: data.fileSize.present ? data.fileSize.value : this.fileSize, + focalLength: + data.focalLength.present ? data.focalLength.value : this.focalLength, + latitude: data.latitude.present ? data.latitude.value : this.latitude, + longitude: data.longitude.present ? data.longitude.value : this.longitude, + iso: data.iso.present ? data.iso.value : this.iso, + make: data.make.present ? data.make.value : this.make, + model: data.model.present ? data.model.value : this.model, + orientation: + data.orientation.present ? data.orientation.value : this.orientation, + timeZone: data.timeZone.present ? data.timeZone.value : this.timeZone, + rating: data.rating.present ? data.rating.value : this.rating, + projectionType: data.projectionType.present + ? data.projectionType.value + : this.projectionType, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityData(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hashAll([ + assetId, + city, + state, + country, + dateTimeOriginal, + description, + height, + width, + exposureTime, + fNumber, + fileSize, + focalLength, + latitude, + longitude, + iso, + make, + model, + orientation, + timeZone, + rating, + projectionType + ]); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.RemoteExifEntityData && + other.assetId == this.assetId && + other.city == this.city && + other.state == this.state && + other.country == this.country && + other.dateTimeOriginal == this.dateTimeOriginal && + other.description == this.description && + other.height == this.height && + other.width == this.width && + other.exposureTime == this.exposureTime && + other.fNumber == this.fNumber && + other.fileSize == this.fileSize && + other.focalLength == this.focalLength && + other.latitude == this.latitude && + other.longitude == this.longitude && + other.iso == this.iso && + other.make == this.make && + other.model == this.model && + other.orientation == this.orientation && + other.timeZone == this.timeZone && + other.rating == this.rating && + other.projectionType == this.projectionType); +} + +class RemoteExifEntityCompanion + extends i0.UpdateCompanion { + final i0.Value assetId; + final i0.Value city; + final i0.Value state; + final i0.Value country; + final i0.Value dateTimeOriginal; + final i0.Value description; + final i0.Value height; + final i0.Value width; + final i0.Value exposureTime; + final i0.Value fNumber; + final i0.Value fileSize; + final i0.Value focalLength; + final i0.Value latitude; + final i0.Value longitude; + final i0.Value iso; + final i0.Value make; + final i0.Value model; + final i0.Value orientation; + final i0.Value timeZone; + final i0.Value rating; + final i0.Value projectionType; + const RemoteExifEntityCompanion({ + this.assetId = const i0.Value.absent(), + this.city = const i0.Value.absent(), + this.state = const i0.Value.absent(), + this.country = const i0.Value.absent(), + this.dateTimeOriginal = const i0.Value.absent(), + this.description = const i0.Value.absent(), + this.height = const i0.Value.absent(), + this.width = const i0.Value.absent(), + this.exposureTime = const i0.Value.absent(), + this.fNumber = const i0.Value.absent(), + this.fileSize = const i0.Value.absent(), + this.focalLength = const i0.Value.absent(), + this.latitude = const i0.Value.absent(), + this.longitude = const i0.Value.absent(), + this.iso = const i0.Value.absent(), + this.make = const i0.Value.absent(), + this.model = const i0.Value.absent(), + this.orientation = const i0.Value.absent(), + this.timeZone = const i0.Value.absent(), + this.rating = const i0.Value.absent(), + this.projectionType = const i0.Value.absent(), + }); + RemoteExifEntityCompanion.insert({ + required String assetId, + this.city = const i0.Value.absent(), + this.state = const i0.Value.absent(), + this.country = const i0.Value.absent(), + this.dateTimeOriginal = const i0.Value.absent(), + this.description = const i0.Value.absent(), + this.height = const i0.Value.absent(), + this.width = const i0.Value.absent(), + this.exposureTime = const i0.Value.absent(), + this.fNumber = const i0.Value.absent(), + this.fileSize = const i0.Value.absent(), + this.focalLength = const i0.Value.absent(), + this.latitude = const i0.Value.absent(), + this.longitude = const i0.Value.absent(), + this.iso = const i0.Value.absent(), + this.make = const i0.Value.absent(), + this.model = const i0.Value.absent(), + this.orientation = const i0.Value.absent(), + this.timeZone = const i0.Value.absent(), + this.rating = const i0.Value.absent(), + this.projectionType = const i0.Value.absent(), + }) : assetId = i0.Value(assetId); + static i0.Insertable custom({ + i0.Expression? assetId, + i0.Expression? city, + i0.Expression? state, + i0.Expression? country, + i0.Expression? dateTimeOriginal, + i0.Expression? description, + i0.Expression? height, + i0.Expression? width, + i0.Expression? exposureTime, + i0.Expression? fNumber, + i0.Expression? fileSize, + i0.Expression? focalLength, + i0.Expression? latitude, + i0.Expression? longitude, + i0.Expression? iso, + i0.Expression? make, + i0.Expression? model, + i0.Expression? orientation, + i0.Expression? timeZone, + i0.Expression? rating, + i0.Expression? projectionType, + }) { + return i0.RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (city != null) 'city': city, + if (state != null) 'state': state, + if (country != null) 'country': country, + if (dateTimeOriginal != null) 'date_time_original': dateTimeOriginal, + if (description != null) 'description': description, + if (height != null) 'height': height, + if (width != null) 'width': width, + if (exposureTime != null) 'exposure_time': exposureTime, + if (fNumber != null) 'f_number': fNumber, + if (fileSize != null) 'file_size': fileSize, + if (focalLength != null) 'focal_length': focalLength, + if (latitude != null) 'latitude': latitude, + if (longitude != null) 'longitude': longitude, + if (iso != null) 'iso': iso, + if (make != null) 'make': make, + if (model != null) 'model': model, + if (orientation != null) 'orientation': orientation, + if (timeZone != null) 'time_zone': timeZone, + if (rating != null) 'rating': rating, + if (projectionType != null) 'projection_type': projectionType, + }); + } + + i1.RemoteExifEntityCompanion copyWith( + {i0.Value? assetId, + i0.Value? city, + i0.Value? state, + i0.Value? country, + i0.Value? dateTimeOriginal, + i0.Value? description, + i0.Value? height, + i0.Value? width, + i0.Value? exposureTime, + i0.Value? fNumber, + i0.Value? fileSize, + i0.Value? focalLength, + i0.Value? latitude, + i0.Value? longitude, + i0.Value? iso, + i0.Value? make, + i0.Value? model, + i0.Value? orientation, + i0.Value? timeZone, + i0.Value? rating, + i0.Value? projectionType}) { + return i1.RemoteExifEntityCompanion( + assetId: assetId ?? this.assetId, + city: city ?? this.city, + state: state ?? this.state, + country: country ?? this.country, + dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal, + description: description ?? this.description, + height: height ?? this.height, + width: width ?? this.width, + exposureTime: exposureTime ?? this.exposureTime, + fNumber: fNumber ?? this.fNumber, + fileSize: fileSize ?? this.fileSize, + focalLength: focalLength ?? this.focalLength, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + iso: iso ?? this.iso, + make: make ?? this.make, + model: model ?? this.model, + orientation: orientation ?? this.orientation, + timeZone: timeZone ?? this.timeZone, + rating: rating ?? this.rating, + projectionType: projectionType ?? this.projectionType, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = i0.Variable(assetId.value); + } + if (city.present) { + map['city'] = i0.Variable(city.value); + } + if (state.present) { + map['state'] = i0.Variable(state.value); + } + if (country.present) { + map['country'] = i0.Variable(country.value); + } + if (dateTimeOriginal.present) { + map['date_time_original'] = i0.Variable(dateTimeOriginal.value); + } + if (description.present) { + map['description'] = i0.Variable(description.value); + } + if (height.present) { + map['height'] = i0.Variable(height.value); + } + if (width.present) { + map['width'] = i0.Variable(width.value); + } + if (exposureTime.present) { + map['exposure_time'] = i0.Variable(exposureTime.value); + } + if (fNumber.present) { + map['f_number'] = i0.Variable(fNumber.value); + } + if (fileSize.present) { + map['file_size'] = i0.Variable(fileSize.value); + } + if (focalLength.present) { + map['focal_length'] = i0.Variable(focalLength.value); + } + if (latitude.present) { + map['latitude'] = i0.Variable(latitude.value); + } + if (longitude.present) { + map['longitude'] = i0.Variable(longitude.value); + } + if (iso.present) { + map['iso'] = i0.Variable(iso.value); + } + if (make.present) { + map['make'] = i0.Variable(make.value); + } + if (model.present) { + map['model'] = i0.Variable(model.value); + } + if (orientation.present) { + map['orientation'] = i0.Variable(orientation.value); + } + if (timeZone.present) { + map['time_zone'] = i0.Variable(timeZone.value); + } + if (rating.present) { + map['rating'] = i0.Variable(rating.value); + } + if (projectionType.present) { + map['projection_type'] = i0.Variable(projectionType.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteExifEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('city: $city, ') + ..write('state: $state, ') + ..write('country: $country, ') + ..write('dateTimeOriginal: $dateTimeOriginal, ') + ..write('description: $description, ') + ..write('height: $height, ') + ..write('width: $width, ') + ..write('exposureTime: $exposureTime, ') + ..write('fNumber: $fNumber, ') + ..write('fileSize: $fileSize, ') + ..write('focalLength: $focalLength, ') + ..write('latitude: $latitude, ') + ..write('longitude: $longitude, ') + ..write('iso: $iso, ') + ..write('make: $make, ') + ..write('model: $model, ') + ..write('orientation: $orientation, ') + ..write('timeZone: $timeZone, ') + ..write('rating: $rating, ') + ..write('projectionType: $projectionType') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/local_asset.entity.dart b/mobile/lib/infrastructure/entities/local_asset.entity.dart index 724cf532c59..ff5ee748181 100644 --- a/mobile/lib/infrastructure/entities/local_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/local_asset.entity.dart @@ -2,7 +2,7 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; -@TableIndex(name: 'local_asset_checksum', columns: {#checksum}) +@TableIndex(name: 'idx_local_asset_checksum', columns: {#checksum}) class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin { const LocalAssetEntity(); diff --git a/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart index 0a4896a4a3a..68bc1b3c5dd 100644 --- a/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart @@ -231,8 +231,8 @@ typedef $$LocalAssetEntityTableProcessedTableManager = i0.ProcessedTableManager< ), i1.LocalAssetEntityData, i0.PrefetchHooks Function()>; -i0.Index get localAssetChecksum => i0.Index('local_asset_checksum', - 'CREATE INDEX local_asset_checksum ON local_asset_entity (checksum)'); +i0.Index get idxLocalAssetChecksum => i0.Index('idx_local_asset_checksum', + 'CREATE INDEX idx_local_asset_checksum ON local_asset_entity (checksum)'); class $LocalAssetEntityTable extends i3.LocalAssetEntity with i0.TableInfo<$LocalAssetEntityTable, i1.LocalAssetEntityData> { diff --git a/mobile/lib/infrastructure/entities/partner.entity.dart b/mobile/lib/infrastructure/entities/partner.entity.dart index b7925a8eeaf..8b51d93e6f7 100644 --- a/mobile/lib/infrastructure/entities/partner.entity.dart +++ b/mobile/lib/infrastructure/entities/partner.entity.dart @@ -5,11 +5,11 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; class PartnerEntity extends Table with DriftDefaultsMixin { const PartnerEntity(); - BlobColumn get sharedById => - blob().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + TextColumn get sharedById => + text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); - BlobColumn get sharedWithId => - blob().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + TextColumn get sharedWithId => + text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); BoolColumn get inTimeline => boolean().withDefault(const Constant(false))(); diff --git a/mobile/lib/infrastructure/entities/partner.entity.drift.dart b/mobile/lib/infrastructure/entities/partner.entity.drift.dart index 974a9e3c303..26a5dd2fe0e 100644 --- a/mobile/lib/infrastructure/entities/partner.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/partner.entity.drift.dart @@ -3,24 +3,23 @@ import 'package:drift/drift.dart' as i0; import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart' as i1; -import 'dart:typed_data' as i2; import 'package:immich_mobile/infrastructure/entities/partner.entity.dart' - as i3; -import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4; + as i2; +import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3; import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart' - as i5; -import 'package:drift/internal/modular.dart' as i6; + as i4; +import 'package:drift/internal/modular.dart' as i5; typedef $$PartnerEntityTableCreateCompanionBuilder = i1.PartnerEntityCompanion Function({ - required i2.Uint8List sharedById, - required i2.Uint8List sharedWithId, + required String sharedById, + required String sharedWithId, i0.Value inTimeline, }); typedef $$PartnerEntityTableUpdateCompanionBuilder = i1.PartnerEntityCompanion Function({ - i0.Value sharedById, - i0.Value sharedWithId, + i0.Value sharedById, + i0.Value sharedWithId, i0.Value inTimeline, }); @@ -29,25 +28,25 @@ final class $$PartnerEntityTableReferences extends i0.BaseReferences< $$PartnerEntityTableReferences( super.$_db, super.$_table, super.$_typedResult); - static i5.$UserEntityTable _sharedByIdTable(i0.GeneratedDatabase db) => - i6.ReadDatabaseContainer(db) - .resultSet('user_entity') + static i4.$UserEntityTable _sharedByIdTable(i0.GeneratedDatabase db) => + i5.ReadDatabaseContainer(db) + .resultSet('user_entity') .createAlias(i0.$_aliasNameGenerator( - i6.ReadDatabaseContainer(db) + i5.ReadDatabaseContainer(db) .resultSet('partner_entity') .sharedById, - i6.ReadDatabaseContainer(db) - .resultSet('user_entity') + i5.ReadDatabaseContainer(db) + .resultSet('user_entity') .id)); - i5.$$UserEntityTableProcessedTableManager get sharedById { - final $_column = $_itemColumn('shared_by_id')!; + i4.$$UserEntityTableProcessedTableManager get sharedById { + final $_column = $_itemColumn('shared_by_id')!; - final manager = i5 + final manager = i4 .$$UserEntityTableTableManager( $_db, - i6.ReadDatabaseContainer($_db) - .resultSet('user_entity')) + i5.ReadDatabaseContainer($_db) + .resultSet('user_entity')) .filter((f) => f.id.sqlEquals($_column)); final item = $_typedResult.readTableOrNull(_sharedByIdTable($_db)); if (item == null) return manager; @@ -55,25 +54,25 @@ final class $$PartnerEntityTableReferences extends i0.BaseReferences< manager.$state.copyWith(prefetchedData: [item])); } - static i5.$UserEntityTable _sharedWithIdTable(i0.GeneratedDatabase db) => - i6.ReadDatabaseContainer(db) - .resultSet('user_entity') + static i4.$UserEntityTable _sharedWithIdTable(i0.GeneratedDatabase db) => + i5.ReadDatabaseContainer(db) + .resultSet('user_entity') .createAlias(i0.$_aliasNameGenerator( - i6.ReadDatabaseContainer(db) + i5.ReadDatabaseContainer(db) .resultSet('partner_entity') .sharedWithId, - i6.ReadDatabaseContainer(db) - .resultSet('user_entity') + i5.ReadDatabaseContainer(db) + .resultSet('user_entity') .id)); - i5.$$UserEntityTableProcessedTableManager get sharedWithId { - final $_column = $_itemColumn('shared_with_id')!; + i4.$$UserEntityTableProcessedTableManager get sharedWithId { + final $_column = $_itemColumn('shared_with_id')!; - final manager = i5 + final manager = i4 .$$UserEntityTableTableManager( $_db, - i6.ReadDatabaseContainer($_db) - .resultSet('user_entity')) + i5.ReadDatabaseContainer($_db) + .resultSet('user_entity')) .filter((f) => f.id.sqlEquals($_column)); final item = $_typedResult.readTableOrNull(_sharedWithIdTable($_db)); if (item == null) return manager; @@ -94,20 +93,20 @@ class $$PartnerEntityTableFilterComposer i0.ColumnFilters get inTimeline => $composableBuilder( column: $table.inTimeline, builder: (column) => i0.ColumnFilters(column)); - i5.$$UserEntityTableFilterComposer get sharedById { - final i5.$$UserEntityTableFilterComposer composer = $composerBuilder( + i4.$$UserEntityTableFilterComposer get sharedById { + final i4.$$UserEntityTableFilterComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.sharedById, - referencedTable: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - i5.$$UserEntityTableFilterComposer( + i4.$$UserEntityTableFilterComposer( $db: $db, - $table: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -116,20 +115,20 @@ class $$PartnerEntityTableFilterComposer return composer; } - i5.$$UserEntityTableFilterComposer get sharedWithId { - final i5.$$UserEntityTableFilterComposer composer = $composerBuilder( + i4.$$UserEntityTableFilterComposer get sharedWithId { + final i4.$$UserEntityTableFilterComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.sharedWithId, - referencedTable: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - i5.$$UserEntityTableFilterComposer( + i4.$$UserEntityTableFilterComposer( $db: $db, - $table: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -152,20 +151,20 @@ class $$PartnerEntityTableOrderingComposer column: $table.inTimeline, builder: (column) => i0.ColumnOrderings(column)); - i5.$$UserEntityTableOrderingComposer get sharedById { - final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder( + i4.$$UserEntityTableOrderingComposer get sharedById { + final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.sharedById, - referencedTable: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - i5.$$UserEntityTableOrderingComposer( + i4.$$UserEntityTableOrderingComposer( $db: $db, - $table: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -174,20 +173,20 @@ class $$PartnerEntityTableOrderingComposer return composer; } - i5.$$UserEntityTableOrderingComposer get sharedWithId { - final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder( + i4.$$UserEntityTableOrderingComposer get sharedWithId { + final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.sharedWithId, - referencedTable: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - i5.$$UserEntityTableOrderingComposer( + i4.$$UserEntityTableOrderingComposer( $db: $db, - $table: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -209,20 +208,20 @@ class $$PartnerEntityTableAnnotationComposer i0.GeneratedColumn get inTimeline => $composableBuilder( column: $table.inTimeline, builder: (column) => column); - i5.$$UserEntityTableAnnotationComposer get sharedById { - final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder( + i4.$$UserEntityTableAnnotationComposer get sharedById { + final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.sharedById, - referencedTable: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - i5.$$UserEntityTableAnnotationComposer( + i4.$$UserEntityTableAnnotationComposer( $db: $db, - $table: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -231,20 +230,20 @@ class $$PartnerEntityTableAnnotationComposer return composer; } - i5.$$UserEntityTableAnnotationComposer get sharedWithId { - final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder( + i4.$$UserEntityTableAnnotationComposer get sharedWithId { + final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.sharedWithId, - referencedTable: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - i5.$$UserEntityTableAnnotationComposer( + i4.$$UserEntityTableAnnotationComposer( $db: $db, - $table: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -278,8 +277,8 @@ class $$PartnerEntityTableTableManager extends i0.RootTableManager< createComputedFieldComposer: () => i1.$$PartnerEntityTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ - i0.Value sharedById = const i0.Value.absent(), - i0.Value sharedWithId = const i0.Value.absent(), + i0.Value sharedById = const i0.Value.absent(), + i0.Value sharedWithId = const i0.Value.absent(), i0.Value inTimeline = const i0.Value.absent(), }) => i1.PartnerEntityCompanion( @@ -288,8 +287,8 @@ class $$PartnerEntityTableTableManager extends i0.RootTableManager< inTimeline: inTimeline, ), createCompanionCallback: ({ - required i2.Uint8List sharedById, - required i2.Uint8List sharedWithId, + required String sharedById, + required String sharedWithId, i0.Value inTimeline = const i0.Value.absent(), }) => i1.PartnerEntityCompanion.insert( @@ -366,7 +365,7 @@ typedef $$PartnerEntityTableProcessedTableManager = i0.ProcessedTableManager< i1.PartnerEntityData, i0.PrefetchHooks Function({bool sharedById, bool sharedWithId})>; -class $PartnerEntityTable extends i3.PartnerEntity +class $PartnerEntityTable extends i2.PartnerEntity with i0.TableInfo<$PartnerEntityTable, i1.PartnerEntityData> { @override final i0.GeneratedDatabase attachedDatabase; @@ -375,18 +374,18 @@ class $PartnerEntityTable extends i3.PartnerEntity static const i0.VerificationMeta _sharedByIdMeta = const i0.VerificationMeta('sharedById'); @override - late final i0.GeneratedColumn sharedById = - i0.GeneratedColumn('shared_by_id', aliasedName, false, - type: i0.DriftSqlType.blob, - requiredDuringInsert: true, - defaultConstraints: i0.GeneratedColumn.constraintIsAlways( - 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final i0.GeneratedColumn sharedById = i0.GeneratedColumn( + 'shared_by_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); static const i0.VerificationMeta _sharedWithIdMeta = const i0.VerificationMeta('sharedWithId'); @override - late final i0.GeneratedColumn sharedWithId = - i0.GeneratedColumn('shared_with_id', aliasedName, false, - type: i0.DriftSqlType.blob, + late final i0.GeneratedColumn sharedWithId = + i0.GeneratedColumn('shared_with_id', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true, defaultConstraints: i0.GeneratedColumn.constraintIsAlways( 'REFERENCES user_entity (id) ON DELETE CASCADE')); @@ -399,7 +398,7 @@ class $PartnerEntityTable extends i3.PartnerEntity requiredDuringInsert: false, defaultConstraints: i0.GeneratedColumn.constraintIsAlways( 'CHECK ("in_timeline" IN (0, 1))'), - defaultValue: const i4.Constant(false)); + defaultValue: const i3.Constant(false)); @override List get $columns => [sharedById, sharedWithId, inTimeline]; @@ -445,10 +444,10 @@ class $PartnerEntityTable extends i3.PartnerEntity i1.PartnerEntityData map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return i1.PartnerEntityData( - sharedById: attachedDatabase.typeMapping - .read(i0.DriftSqlType.blob, data['${effectivePrefix}shared_by_id'])!, + sharedById: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, data['${effectivePrefix}shared_by_id'])!, sharedWithId: attachedDatabase.typeMapping.read( - i0.DriftSqlType.blob, data['${effectivePrefix}shared_with_id'])!, + i0.DriftSqlType.string, data['${effectivePrefix}shared_with_id'])!, inTimeline: attachedDatabase.typeMapping .read(i0.DriftSqlType.bool, data['${effectivePrefix}in_timeline'])!, ); @@ -467,8 +466,8 @@ class $PartnerEntityTable extends i3.PartnerEntity class PartnerEntityData extends i0.DataClass implements i0.Insertable { - final i2.Uint8List sharedById; - final i2.Uint8List sharedWithId; + final String sharedById; + final String sharedWithId; final bool inTimeline; const PartnerEntityData( {required this.sharedById, @@ -477,8 +476,8 @@ class PartnerEntityData extends i0.DataClass @override Map toColumns(bool nullToAbsent) { final map = {}; - map['shared_by_id'] = i0.Variable(sharedById); - map['shared_with_id'] = i0.Variable(sharedWithId); + map['shared_by_id'] = i0.Variable(sharedById); + map['shared_with_id'] = i0.Variable(sharedWithId); map['in_timeline'] = i0.Variable(inTimeline); return map; } @@ -487,8 +486,8 @@ class PartnerEntityData extends i0.DataClass {i0.ValueSerializer? serializer}) { serializer ??= i0.driftRuntimeOptions.defaultSerializer; return PartnerEntityData( - sharedById: serializer.fromJson(json['sharedById']), - sharedWithId: serializer.fromJson(json['sharedWithId']), + sharedById: serializer.fromJson(json['sharedById']), + sharedWithId: serializer.fromJson(json['sharedWithId']), inTimeline: serializer.fromJson(json['inTimeline']), ); } @@ -496,16 +495,14 @@ class PartnerEntityData extends i0.DataClass Map toJson({i0.ValueSerializer? serializer}) { serializer ??= i0.driftRuntimeOptions.defaultSerializer; return { - 'sharedById': serializer.toJson(sharedById), - 'sharedWithId': serializer.toJson(sharedWithId), + 'sharedById': serializer.toJson(sharedById), + 'sharedWithId': serializer.toJson(sharedWithId), 'inTimeline': serializer.toJson(inTimeline), }; } i1.PartnerEntityData copyWith( - {i2.Uint8List? sharedById, - i2.Uint8List? sharedWithId, - bool? inTimeline}) => + {String? sharedById, String? sharedWithId, bool? inTimeline}) => i1.PartnerEntityData( sharedById: sharedById ?? this.sharedById, sharedWithId: sharedWithId ?? this.sharedWithId, @@ -534,20 +531,19 @@ class PartnerEntityData extends i0.DataClass } @override - int get hashCode => Object.hash(i0.$driftBlobEquality.hash(sharedById), - i0.$driftBlobEquality.hash(sharedWithId), inTimeline); + int get hashCode => Object.hash(sharedById, sharedWithId, inTimeline); @override bool operator ==(Object other) => identical(this, other) || (other is i1.PartnerEntityData && - i0.$driftBlobEquality.equals(other.sharedById, this.sharedById) && - i0.$driftBlobEquality.equals(other.sharedWithId, this.sharedWithId) && + other.sharedById == this.sharedById && + other.sharedWithId == this.sharedWithId && other.inTimeline == this.inTimeline); } class PartnerEntityCompanion extends i0.UpdateCompanion { - final i0.Value sharedById; - final i0.Value sharedWithId; + final i0.Value sharedById; + final i0.Value sharedWithId; final i0.Value inTimeline; const PartnerEntityCompanion({ this.sharedById = const i0.Value.absent(), @@ -555,14 +551,14 @@ class PartnerEntityCompanion extends i0.UpdateCompanion { this.inTimeline = const i0.Value.absent(), }); PartnerEntityCompanion.insert({ - required i2.Uint8List sharedById, - required i2.Uint8List sharedWithId, + required String sharedById, + required String sharedWithId, this.inTimeline = const i0.Value.absent(), }) : sharedById = i0.Value(sharedById), sharedWithId = i0.Value(sharedWithId); static i0.Insertable custom({ - i0.Expression? sharedById, - i0.Expression? sharedWithId, + i0.Expression? sharedById, + i0.Expression? sharedWithId, i0.Expression? inTimeline, }) { return i0.RawValuesInsertable({ @@ -573,8 +569,8 @@ class PartnerEntityCompanion extends i0.UpdateCompanion { } i1.PartnerEntityCompanion copyWith( - {i0.Value? sharedById, - i0.Value? sharedWithId, + {i0.Value? sharedById, + i0.Value? sharedWithId, i0.Value? inTimeline}) { return i1.PartnerEntityCompanion( sharedById: sharedById ?? this.sharedById, @@ -587,10 +583,10 @@ class PartnerEntityCompanion extends i0.UpdateCompanion { Map toColumns(bool nullToAbsent) { final map = {}; if (sharedById.present) { - map['shared_by_id'] = i0.Variable(sharedById.value); + map['shared_by_id'] = i0.Variable(sharedById.value); } if (sharedWithId.present) { - map['shared_with_id'] = i0.Variable(sharedWithId.value); + map['shared_with_id'] = i0.Variable(sharedWithId.value); } if (inTimeline.present) { map['in_timeline'] = i0.Variable(inTimeline.value); diff --git a/mobile/lib/infrastructure/entities/remote_asset.entity.dart b/mobile/lib/infrastructure/entities/remote_asset.entity.dart new file mode 100644 index 00000000000..96f4077a2ae --- /dev/null +++ b/mobile/lib/infrastructure/entities/remote_asset.entity.dart @@ -0,0 +1,35 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +@TableIndex( + name: 'UQ_remote_asset_owner_checksum', + columns: {#checksum, #ownerId}, + unique: true, +) +class RemoteAssetEntity extends Table + with DriftDefaultsMixin, AssetEntityMixin { + const RemoteAssetEntity(); + + TextColumn get id => text()(); + + TextColumn get checksum => text()(); + + BoolColumn get isFavorite => boolean().withDefault(const Constant(false))(); + + TextColumn get ownerId => + text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + + DateTimeColumn get localDateTime => dateTime().nullable()(); + + TextColumn get thumbHash => text().nullable()(); + + DateTimeColumn get deletedAt => dateTime().nullable()(); + + IntColumn get visibility => intEnum()(); + + @override + Set get primaryKey => {id}; +} diff --git a/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart new file mode 100644 index 00000000000..e3fe521700e --- /dev/null +++ b/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart @@ -0,0 +1,1076 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart' + as i1; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as i2; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart' + as i3; +import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4; +import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart' + as i5; +import 'package:drift/internal/modular.dart' as i6; + +typedef $$RemoteAssetEntityTableCreateCompanionBuilder + = i1.RemoteAssetEntityCompanion Function({ + required String name, + required i2.AssetType type, + i0.Value createdAt, + i0.Value updatedAt, + i0.Value durationInSeconds, + required String id, + required String checksum, + i0.Value isFavorite, + required String ownerId, + i0.Value localDateTime, + i0.Value thumbHash, + i0.Value deletedAt, + required i2.AssetVisibility visibility, +}); +typedef $$RemoteAssetEntityTableUpdateCompanionBuilder + = i1.RemoteAssetEntityCompanion Function({ + i0.Value name, + i0.Value type, + i0.Value createdAt, + i0.Value updatedAt, + i0.Value durationInSeconds, + i0.Value id, + i0.Value checksum, + i0.Value isFavorite, + i0.Value ownerId, + i0.Value localDateTime, + i0.Value thumbHash, + i0.Value deletedAt, + i0.Value visibility, +}); + +final class $$RemoteAssetEntityTableReferences extends i0.BaseReferences< + i0.GeneratedDatabase, + i1.$RemoteAssetEntityTable, + i1.RemoteAssetEntityData> { + $$RemoteAssetEntityTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static i5.$UserEntityTable _ownerIdTable(i0.GeneratedDatabase db) => + i6.ReadDatabaseContainer(db) + .resultSet('user_entity') + .createAlias(i0.$_aliasNameGenerator( + i6.ReadDatabaseContainer(db) + .resultSet('remote_asset_entity') + .ownerId, + i6.ReadDatabaseContainer(db) + .resultSet('user_entity') + .id)); + + i5.$$UserEntityTableProcessedTableManager get ownerId { + final $_column = $_itemColumn('owner_id')!; + + final manager = i5 + .$$UserEntityTableTableManager( + $_db, + i6.ReadDatabaseContainer($_db) + .resultSet('user_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_ownerIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$RemoteAssetEntityTableFilterComposer + extends i0.Composer { + $$RemoteAssetEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnFilters get name => $composableBuilder( + column: $table.name, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnWithTypeConverterFilters get type => + $composableBuilder( + column: $table.type, + builder: (column) => i0.ColumnWithTypeConverterFilters(column)); + + i0.ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get durationInSeconds => $composableBuilder( + column: $table.durationInSeconds, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get checksum => $composableBuilder( + column: $table.checksum, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get isFavorite => $composableBuilder( + column: $table.isFavorite, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get localDateTime => $composableBuilder( + column: $table.localDateTime, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get thumbHash => $composableBuilder( + column: $table.thumbHash, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get deletedAt => $composableBuilder( + column: $table.deletedAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnWithTypeConverterFilters + get visibility => $composableBuilder( + column: $table.visibility, + builder: (column) => i0.ColumnWithTypeConverterFilters(column)); + + i5.$$UserEntityTableFilterComposer get ownerId { + final i5.$$UserEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.ownerId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$UserEntityTableFilterComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteAssetEntityTableOrderingComposer + extends i0.Composer { + $$RemoteAssetEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get name => $composableBuilder( + column: $table.name, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get type => $composableBuilder( + column: $table.type, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get durationInSeconds => $composableBuilder( + column: $table.durationInSeconds, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get checksum => $composableBuilder( + column: $table.checksum, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get isFavorite => $composableBuilder( + column: $table.isFavorite, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get localDateTime => $composableBuilder( + column: $table.localDateTime, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get thumbHash => $composableBuilder( + column: $table.thumbHash, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get deletedAt => $composableBuilder( + column: $table.deletedAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get visibility => $composableBuilder( + column: $table.visibility, + builder: (column) => i0.ColumnOrderings(column)); + + i5.$$UserEntityTableOrderingComposer get ownerId { + final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.ownerId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$UserEntityTableOrderingComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteAssetEntityTableAnnotationComposer + extends i0.Composer { + $$RemoteAssetEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + i0.GeneratedColumnWithTypeConverter get type => + $composableBuilder(column: $table.type, builder: (column) => column); + + i0.GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + i0.GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + i0.GeneratedColumn get durationInSeconds => $composableBuilder( + column: $table.durationInSeconds, builder: (column) => column); + + i0.GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + i0.GeneratedColumn get checksum => + $composableBuilder(column: $table.checksum, builder: (column) => column); + + i0.GeneratedColumn get isFavorite => $composableBuilder( + column: $table.isFavorite, builder: (column) => column); + + i0.GeneratedColumn get localDateTime => $composableBuilder( + column: $table.localDateTime, builder: (column) => column); + + i0.GeneratedColumn get thumbHash => + $composableBuilder(column: $table.thumbHash, builder: (column) => column); + + i0.GeneratedColumn get deletedAt => + $composableBuilder(column: $table.deletedAt, builder: (column) => column); + + i0.GeneratedColumnWithTypeConverter get visibility => + $composableBuilder( + column: $table.visibility, builder: (column) => column); + + i5.$$UserEntityTableAnnotationComposer get ownerId { + final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.ownerId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$UserEntityTableAnnotationComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$RemoteAssetEntityTable, + i1.RemoteAssetEntityData, + i1.$$RemoteAssetEntityTableFilterComposer, + i1.$$RemoteAssetEntityTableOrderingComposer, + i1.$$RemoteAssetEntityTableAnnotationComposer, + $$RemoteAssetEntityTableCreateCompanionBuilder, + $$RemoteAssetEntityTableUpdateCompanionBuilder, + (i1.RemoteAssetEntityData, i1.$$RemoteAssetEntityTableReferences), + i1.RemoteAssetEntityData, + i0.PrefetchHooks Function({bool ownerId})> { + $$RemoteAssetEntityTableTableManager( + i0.GeneratedDatabase db, i1.$RemoteAssetEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$RemoteAssetEntityTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => i1 + .$$RemoteAssetEntityTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + i1.$$RemoteAssetEntityTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + i0.Value name = const i0.Value.absent(), + i0.Value type = const i0.Value.absent(), + i0.Value createdAt = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + i0.Value durationInSeconds = const i0.Value.absent(), + i0.Value id = const i0.Value.absent(), + i0.Value checksum = const i0.Value.absent(), + i0.Value isFavorite = const i0.Value.absent(), + i0.Value ownerId = const i0.Value.absent(), + i0.Value localDateTime = const i0.Value.absent(), + i0.Value thumbHash = const i0.Value.absent(), + i0.Value deletedAt = const i0.Value.absent(), + i0.Value visibility = const i0.Value.absent(), + }) => + i1.RemoteAssetEntityCompanion( + name: name, + type: type, + createdAt: createdAt, + updatedAt: updatedAt, + durationInSeconds: durationInSeconds, + id: id, + checksum: checksum, + isFavorite: isFavorite, + ownerId: ownerId, + localDateTime: localDateTime, + thumbHash: thumbHash, + deletedAt: deletedAt, + visibility: visibility, + ), + createCompanionCallback: ({ + required String name, + required i2.AssetType type, + i0.Value createdAt = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + i0.Value durationInSeconds = const i0.Value.absent(), + required String id, + required String checksum, + i0.Value isFavorite = const i0.Value.absent(), + required String ownerId, + i0.Value localDateTime = const i0.Value.absent(), + i0.Value thumbHash = const i0.Value.absent(), + i0.Value deletedAt = const i0.Value.absent(), + required i2.AssetVisibility visibility, + }) => + i1.RemoteAssetEntityCompanion.insert( + name: name, + type: type, + createdAt: createdAt, + updatedAt: updatedAt, + durationInSeconds: durationInSeconds, + id: id, + checksum: checksum, + isFavorite: isFavorite, + ownerId: ownerId, + localDateTime: localDateTime, + thumbHash: thumbHash, + deletedAt: deletedAt, + visibility: visibility, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + i1.$$RemoteAssetEntityTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({ownerId = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends i0.TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (ownerId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.ownerId, + referencedTable: + i1.$$RemoteAssetEntityTableReferences._ownerIdTable(db), + referencedColumn: i1.$$RemoteAssetEntityTableReferences + ._ownerIdTable(db) + .id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$RemoteAssetEntityTableProcessedTableManager + = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$RemoteAssetEntityTable, + i1.RemoteAssetEntityData, + i1.$$RemoteAssetEntityTableFilterComposer, + i1.$$RemoteAssetEntityTableOrderingComposer, + i1.$$RemoteAssetEntityTableAnnotationComposer, + $$RemoteAssetEntityTableCreateCompanionBuilder, + $$RemoteAssetEntityTableUpdateCompanionBuilder, + (i1.RemoteAssetEntityData, i1.$$RemoteAssetEntityTableReferences), + i1.RemoteAssetEntityData, + i0.PrefetchHooks Function({bool ownerId})>; +i0.Index get uQRemoteAssetOwnerChecksum => i0.Index( + 'UQ_remote_asset_owner_checksum', + 'CREATE UNIQUE INDEX UQ_remote_asset_owner_checksum ON remote_asset_entity (checksum, owner_id)'); + +class $RemoteAssetEntityTable extends i3.RemoteAssetEntity + with i0.TableInfo<$RemoteAssetEntityTable, i1.RemoteAssetEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $RemoteAssetEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _nameMeta = + const i0.VerificationMeta('name'); + @override + late final i0.GeneratedColumn name = i0.GeneratedColumn( + 'name', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + @override + late final i0.GeneratedColumnWithTypeConverter type = + i0.GeneratedColumn('type', aliasedName, false, + type: i0.DriftSqlType.int, requiredDuringInsert: true) + .withConverter( + i1.$RemoteAssetEntityTable.$convertertype); + static const i0.VerificationMeta _createdAtMeta = + const i0.VerificationMeta('createdAt'); + @override + late final i0.GeneratedColumn createdAt = + i0.GeneratedColumn('created_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i4.currentDateAndTime); + static const i0.VerificationMeta _updatedAtMeta = + const i0.VerificationMeta('updatedAt'); + @override + late final i0.GeneratedColumn updatedAt = + i0.GeneratedColumn('updated_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i4.currentDateAndTime); + static const i0.VerificationMeta _durationInSecondsMeta = + const i0.VerificationMeta('durationInSeconds'); + @override + late final i0.GeneratedColumn durationInSeconds = + i0.GeneratedColumn('duration_in_seconds', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id'); + @override + late final i0.GeneratedColumn id = i0.GeneratedColumn( + 'id', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _checksumMeta = + const i0.VerificationMeta('checksum'); + @override + late final i0.GeneratedColumn checksum = i0.GeneratedColumn( + 'checksum', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _isFavoriteMeta = + const i0.VerificationMeta('isFavorite'); + @override + late final i0.GeneratedColumn isFavorite = i0.GeneratedColumn( + 'is_favorite', aliasedName, false, + type: i0.DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'CHECK ("is_favorite" IN (0, 1))'), + defaultValue: const i4.Constant(false)); + static const i0.VerificationMeta _ownerIdMeta = + const i0.VerificationMeta('ownerId'); + @override + late final i0.GeneratedColumn ownerId = i0.GeneratedColumn( + 'owner_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + static const i0.VerificationMeta _localDateTimeMeta = + const i0.VerificationMeta('localDateTime'); + @override + late final i0.GeneratedColumn localDateTime = + i0.GeneratedColumn('local_date_time', aliasedName, true, + type: i0.DriftSqlType.dateTime, requiredDuringInsert: false); + static const i0.VerificationMeta _thumbHashMeta = + const i0.VerificationMeta('thumbHash'); + @override + late final i0.GeneratedColumn thumbHash = i0.GeneratedColumn( + 'thumb_hash', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + static const i0.VerificationMeta _deletedAtMeta = + const i0.VerificationMeta('deletedAt'); + @override + late final i0.GeneratedColumn deletedAt = + i0.GeneratedColumn('deleted_at', aliasedName, true, + type: i0.DriftSqlType.dateTime, requiredDuringInsert: false); + @override + late final i0.GeneratedColumnWithTypeConverter + visibility = i0.GeneratedColumn('visibility', aliasedName, false, + type: i0.DriftSqlType.int, requiredDuringInsert: true) + .withConverter( + i1.$RemoteAssetEntityTable.$convertervisibility); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + visibility + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_asset_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('name')) { + context.handle( + _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + if (data.containsKey('updated_at')) { + context.handle(_updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); + } + if (data.containsKey('duration_in_seconds')) { + context.handle( + _durationInSecondsMeta, + durationInSeconds.isAcceptableOrUnknown( + data['duration_in_seconds']!, _durationInSecondsMeta)); + } + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('checksum')) { + context.handle(_checksumMeta, + checksum.isAcceptableOrUnknown(data['checksum']!, _checksumMeta)); + } else if (isInserting) { + context.missing(_checksumMeta); + } + if (data.containsKey('is_favorite')) { + context.handle( + _isFavoriteMeta, + isFavorite.isAcceptableOrUnknown( + data['is_favorite']!, _isFavoriteMeta)); + } + if (data.containsKey('owner_id')) { + context.handle(_ownerIdMeta, + ownerId.isAcceptableOrUnknown(data['owner_id']!, _ownerIdMeta)); + } else if (isInserting) { + context.missing(_ownerIdMeta); + } + if (data.containsKey('local_date_time')) { + context.handle( + _localDateTimeMeta, + localDateTime.isAcceptableOrUnknown( + data['local_date_time']!, _localDateTimeMeta)); + } + if (data.containsKey('thumb_hash')) { + context.handle(_thumbHashMeta, + thumbHash.isAcceptableOrUnknown(data['thumb_hash']!, _thumbHashMeta)); + } + if (data.containsKey('deleted_at')) { + context.handle(_deletedAtMeta, + deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta)); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + i1.RemoteAssetEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.RemoteAssetEntityData( + name: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!, + type: i1.$RemoteAssetEntityTable.$convertertype.fromSql(attachedDatabase + .typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}type'])!), + createdAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + durationInSeconds: attachedDatabase.typeMapping.read( + i0.DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']), + id: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!, + checksum: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}checksum'])!, + isFavorite: attachedDatabase.typeMapping + .read(i0.DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!, + ownerId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + localDateTime: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}local_date_time']), + thumbHash: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}thumb_hash']), + deletedAt: attachedDatabase.typeMapping + .read(i0.DriftSqlType.dateTime, data['${effectivePrefix}deleted_at']), + visibility: i1.$RemoteAssetEntityTable.$convertervisibility.fromSql( + attachedDatabase.typeMapping.read( + i0.DriftSqlType.int, data['${effectivePrefix}visibility'])!), + ); + } + + @override + $RemoteAssetEntityTable createAlias(String alias) { + return $RemoteAssetEntityTable(attachedDatabase, alias); + } + + static i0.JsonTypeConverter2 $convertertype = + const i0.EnumIndexConverter(i2.AssetType.values); + static i0.JsonTypeConverter2 + $convertervisibility = const i0.EnumIndexConverter( + i2.AssetVisibility.values); + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAssetEntityData extends i0.DataClass + implements i0.Insertable { + final String name; + final i2.AssetType type; + final DateTime createdAt; + final DateTime updatedAt; + final int? durationInSeconds; + final String id; + final String checksum; + final bool isFavorite; + final String ownerId; + final DateTime? localDateTime; + final String? thumbHash; + final DateTime? deletedAt; + final i2.AssetVisibility visibility; + const RemoteAssetEntityData( + {required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.durationInSeconds, + required this.id, + required this.checksum, + required this.isFavorite, + required this.ownerId, + this.localDateTime, + this.thumbHash, + this.deletedAt, + required this.visibility}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = i0.Variable(name); + { + map['type'] = i0.Variable( + i1.$RemoteAssetEntityTable.$convertertype.toSql(type)); + } + map['created_at'] = i0.Variable(createdAt); + map['updated_at'] = i0.Variable(updatedAt); + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = i0.Variable(durationInSeconds); + } + map['id'] = i0.Variable(id); + map['checksum'] = i0.Variable(checksum); + map['is_favorite'] = i0.Variable(isFavorite); + map['owner_id'] = i0.Variable(ownerId); + if (!nullToAbsent || localDateTime != null) { + map['local_date_time'] = i0.Variable(localDateTime); + } + if (!nullToAbsent || thumbHash != null) { + map['thumb_hash'] = i0.Variable(thumbHash); + } + if (!nullToAbsent || deletedAt != null) { + map['deleted_at'] = i0.Variable(deletedAt); + } + { + map['visibility'] = i0.Variable( + i1.$RemoteAssetEntityTable.$convertervisibility.toSql(visibility)); + } + return map; + } + + factory RemoteAssetEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return RemoteAssetEntityData( + name: serializer.fromJson(json['name']), + type: i1.$RemoteAssetEntityTable.$convertertype + .fromJson(serializer.fromJson(json['type'])), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + id: serializer.fromJson(json['id']), + checksum: serializer.fromJson(json['checksum']), + isFavorite: serializer.fromJson(json['isFavorite']), + ownerId: serializer.fromJson(json['ownerId']), + localDateTime: serializer.fromJson(json['localDateTime']), + thumbHash: serializer.fromJson(json['thumbHash']), + deletedAt: serializer.fromJson(json['deletedAt']), + visibility: i1.$RemoteAssetEntityTable.$convertervisibility + .fromJson(serializer.fromJson(json['visibility'])), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer + .toJson(i1.$RemoteAssetEntityTable.$convertertype.toJson(type)), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'id': serializer.toJson(id), + 'checksum': serializer.toJson(checksum), + 'isFavorite': serializer.toJson(isFavorite), + 'ownerId': serializer.toJson(ownerId), + 'localDateTime': serializer.toJson(localDateTime), + 'thumbHash': serializer.toJson(thumbHash), + 'deletedAt': serializer.toJson(deletedAt), + 'visibility': serializer.toJson( + i1.$RemoteAssetEntityTable.$convertervisibility.toJson(visibility)), + }; + } + + i1.RemoteAssetEntityData copyWith( + {String? name, + i2.AssetType? type, + DateTime? createdAt, + DateTime? updatedAt, + i0.Value durationInSeconds = const i0.Value.absent(), + String? id, + String? checksum, + bool? isFavorite, + String? ownerId, + i0.Value localDateTime = const i0.Value.absent(), + i0.Value thumbHash = const i0.Value.absent(), + i0.Value deletedAt = const i0.Value.absent(), + i2.AssetVisibility? visibility}) => + i1.RemoteAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: + localDateTime.present ? localDateTime.value : this.localDateTime, + thumbHash: thumbHash.present ? thumbHash.value : this.thumbHash, + deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt, + visibility: visibility ?? this.visibility, + ); + RemoteAssetEntityData copyWithCompanion(i1.RemoteAssetEntityCompanion data) { + return RemoteAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + id: data.id.present ? data.id.value : this.id, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + isFavorite: + data.isFavorite.present ? data.isFavorite.value : this.isFavorite, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + localDateTime: data.localDateTime.present + ? data.localDateTime.value + : this.localDateTime, + thumbHash: data.thumbHash.present ? data.thumbHash.value : this.thumbHash, + deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt, + visibility: + data.visibility.present ? data.visibility.value : this.visibility, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('visibility: $visibility') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + name, + type, + createdAt, + updatedAt, + durationInSeconds, + id, + checksum, + isFavorite, + ownerId, + localDateTime, + thumbHash, + deletedAt, + visibility); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.RemoteAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.durationInSeconds == this.durationInSeconds && + other.id == this.id && + other.checksum == this.checksum && + other.isFavorite == this.isFavorite && + other.ownerId == this.ownerId && + other.localDateTime == this.localDateTime && + other.thumbHash == this.thumbHash && + other.deletedAt == this.deletedAt && + other.visibility == this.visibility); +} + +class RemoteAssetEntityCompanion + extends i0.UpdateCompanion { + final i0.Value name; + final i0.Value type; + final i0.Value createdAt; + final i0.Value updatedAt; + final i0.Value durationInSeconds; + final i0.Value id; + final i0.Value checksum; + final i0.Value isFavorite; + final i0.Value ownerId; + final i0.Value localDateTime; + final i0.Value thumbHash; + final i0.Value deletedAt; + final i0.Value visibility; + const RemoteAssetEntityCompanion({ + this.name = const i0.Value.absent(), + this.type = const i0.Value.absent(), + this.createdAt = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + this.durationInSeconds = const i0.Value.absent(), + this.id = const i0.Value.absent(), + this.checksum = const i0.Value.absent(), + this.isFavorite = const i0.Value.absent(), + this.ownerId = const i0.Value.absent(), + this.localDateTime = const i0.Value.absent(), + this.thumbHash = const i0.Value.absent(), + this.deletedAt = const i0.Value.absent(), + this.visibility = const i0.Value.absent(), + }); + RemoteAssetEntityCompanion.insert({ + required String name, + required i2.AssetType type, + this.createdAt = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + this.durationInSeconds = const i0.Value.absent(), + required String id, + required String checksum, + this.isFavorite = const i0.Value.absent(), + required String ownerId, + this.localDateTime = const i0.Value.absent(), + this.thumbHash = const i0.Value.absent(), + this.deletedAt = const i0.Value.absent(), + required i2.AssetVisibility visibility, + }) : name = i0.Value(name), + type = i0.Value(type), + id = i0.Value(id), + checksum = i0.Value(checksum), + ownerId = i0.Value(ownerId), + visibility = i0.Value(visibility); + static i0.Insertable custom({ + i0.Expression? name, + i0.Expression? type, + i0.Expression? createdAt, + i0.Expression? updatedAt, + i0.Expression? durationInSeconds, + i0.Expression? id, + i0.Expression? checksum, + i0.Expression? isFavorite, + i0.Expression? ownerId, + i0.Expression? localDateTime, + i0.Expression? thumbHash, + i0.Expression? deletedAt, + i0.Expression? visibility, + }) { + return i0.RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (id != null) 'id': id, + if (checksum != null) 'checksum': checksum, + if (isFavorite != null) 'is_favorite': isFavorite, + if (ownerId != null) 'owner_id': ownerId, + if (localDateTime != null) 'local_date_time': localDateTime, + if (thumbHash != null) 'thumb_hash': thumbHash, + if (deletedAt != null) 'deleted_at': deletedAt, + if (visibility != null) 'visibility': visibility, + }); + } + + i1.RemoteAssetEntityCompanion copyWith( + {i0.Value? name, + i0.Value? type, + i0.Value? createdAt, + i0.Value? updatedAt, + i0.Value? durationInSeconds, + i0.Value? id, + i0.Value? checksum, + i0.Value? isFavorite, + i0.Value? ownerId, + i0.Value? localDateTime, + i0.Value? thumbHash, + i0.Value? deletedAt, + i0.Value? visibility}) { + return i1.RemoteAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + id: id ?? this.id, + checksum: checksum ?? this.checksum, + isFavorite: isFavorite ?? this.isFavorite, + ownerId: ownerId ?? this.ownerId, + localDateTime: localDateTime ?? this.localDateTime, + thumbHash: thumbHash ?? this.thumbHash, + deletedAt: deletedAt ?? this.deletedAt, + visibility: visibility ?? this.visibility, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = i0.Variable(name.value); + } + if (type.present) { + map['type'] = i0.Variable( + i1.$RemoteAssetEntityTable.$convertertype.toSql(type.value)); + } + if (createdAt.present) { + map['created_at'] = i0.Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = i0.Variable(updatedAt.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = i0.Variable(durationInSeconds.value); + } + if (id.present) { + map['id'] = i0.Variable(id.value); + } + if (checksum.present) { + map['checksum'] = i0.Variable(checksum.value); + } + if (isFavorite.present) { + map['is_favorite'] = i0.Variable(isFavorite.value); + } + if (ownerId.present) { + map['owner_id'] = i0.Variable(ownerId.value); + } + if (localDateTime.present) { + map['local_date_time'] = i0.Variable(localDateTime.value); + } + if (thumbHash.present) { + map['thumb_hash'] = i0.Variable(thumbHash.value); + } + if (deletedAt.present) { + map['deleted_at'] = i0.Variable(deletedAt.value); + } + if (visibility.present) { + map['visibility'] = i0.Variable(i1 + .$RemoteAssetEntityTable.$convertervisibility + .toSql(visibility.value)); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('id: $id, ') + ..write('checksum: $checksum, ') + ..write('isFavorite: $isFavorite, ') + ..write('ownerId: $ownerId, ') + ..write('localDateTime: $localDateTime, ') + ..write('thumbHash: $thumbHash, ') + ..write('deletedAt: $deletedAt, ') + ..write('visibility: $visibility') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/user.entity.dart b/mobile/lib/infrastructure/entities/user.entity.dart index 955b2267d1f..b0c1e6e866d 100644 --- a/mobile/lib/infrastructure/entities/user.entity.dart +++ b/mobile/lib/infrastructure/entities/user.entity.dart @@ -78,7 +78,7 @@ class User { class UserEntity extends Table with DriftDefaultsMixin { const UserEntity(); - BlobColumn get id => blob()(); + TextColumn get id => text()(); TextColumn get name => text()(); BoolColumn get isAdmin => boolean().withDefault(const Constant(false))(); TextColumn get email => text()(); diff --git a/mobile/lib/infrastructure/entities/user.entity.drift.dart b/mobile/lib/infrastructure/entities/user.entity.drift.dart index 474746a7921..32be9695182 100644 --- a/mobile/lib/infrastructure/entities/user.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/user.entity.drift.dart @@ -3,13 +3,12 @@ import 'package:drift/drift.dart' as i0; import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart' as i1; -import 'dart:typed_data' as i2; -import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as i3; -import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as i2; +import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3; typedef $$UserEntityTableCreateCompanionBuilder = i1.UserEntityCompanion Function({ - required i2.Uint8List id, + required String id, required String name, i0.Value isAdmin, required String email, @@ -20,7 +19,7 @@ typedef $$UserEntityTableCreateCompanionBuilder = i1.UserEntityCompanion }); typedef $$UserEntityTableUpdateCompanionBuilder = i1.UserEntityCompanion Function({ - i0.Value id, + i0.Value id, i0.Value name, i0.Value isAdmin, i0.Value email, @@ -39,7 +38,7 @@ class $$UserEntityTableFilterComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - i0.ColumnFilters get id => $composableBuilder( + i0.ColumnFilters get id => $composableBuilder( column: $table.id, builder: (column) => i0.ColumnFilters(column)); i0.ColumnFilters get name => $composableBuilder( @@ -76,7 +75,7 @@ class $$UserEntityTableOrderingComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - i0.ColumnOrderings get id => $composableBuilder( + i0.ColumnOrderings get id => $composableBuilder( column: $table.id, builder: (column) => i0.ColumnOrderings(column)); i0.ColumnOrderings get name => $composableBuilder( @@ -114,7 +113,7 @@ class $$UserEntityTableAnnotationComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - i0.GeneratedColumn get id => + i0.GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); i0.GeneratedColumn get name => @@ -167,7 +166,7 @@ class $$UserEntityTableTableManager extends i0.RootTableManager< createComputedFieldComposer: () => i1.$$UserEntityTableAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ - i0.Value id = const i0.Value.absent(), + i0.Value id = const i0.Value.absent(), i0.Value name = const i0.Value.absent(), i0.Value isAdmin = const i0.Value.absent(), i0.Value email = const i0.Value.absent(), @@ -187,7 +186,7 @@ class $$UserEntityTableTableManager extends i0.RootTableManager< quotaUsageInBytes: quotaUsageInBytes, ), createCompanionCallback: ({ - required i2.Uint8List id, + required String id, required String name, i0.Value isAdmin = const i0.Value.absent(), required String email, @@ -230,7 +229,7 @@ typedef $$UserEntityTableProcessedTableManager = i0.ProcessedTableManager< i1.UserEntityData, i0.PrefetchHooks Function()>; -class $UserEntityTable extends i3.UserEntity +class $UserEntityTable extends i2.UserEntity with i0.TableInfo<$UserEntityTable, i1.UserEntityData> { @override final i0.GeneratedDatabase attachedDatabase; @@ -238,9 +237,9 @@ class $UserEntityTable extends i3.UserEntity $UserEntityTable(this.attachedDatabase, [this._alias]); static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id'); @override - late final i0.GeneratedColumn id = - i0.GeneratedColumn('id', aliasedName, false, - type: i0.DriftSqlType.blob, requiredDuringInsert: true); + late final i0.GeneratedColumn id = i0.GeneratedColumn( + 'id', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); static const i0.VerificationMeta _nameMeta = const i0.VerificationMeta('name'); @override @@ -256,7 +255,7 @@ class $UserEntityTable extends i3.UserEntity requiredDuringInsert: false, defaultConstraints: i0.GeneratedColumn.constraintIsAlways('CHECK ("is_admin" IN (0, 1))'), - defaultValue: const i4.Constant(false)); + defaultValue: const i3.Constant(false)); static const i0.VerificationMeta _emailMeta = const i0.VerificationMeta('email'); @override @@ -276,7 +275,7 @@ class $UserEntityTable extends i3.UserEntity i0.GeneratedColumn('updated_at', aliasedName, false, type: i0.DriftSqlType.dateTime, requiredDuringInsert: false, - defaultValue: i4.currentDateAndTime); + defaultValue: i3.currentDateAndTime); static const i0.VerificationMeta _quotaSizeInBytesMeta = const i0.VerificationMeta('quotaSizeInBytes'); @override @@ -290,7 +289,7 @@ class $UserEntityTable extends i3.UserEntity i0.GeneratedColumn('quota_usage_in_bytes', aliasedName, false, type: i0.DriftSqlType.int, requiredDuringInsert: false, - defaultValue: const i4.Constant(0)); + defaultValue: const i3.Constant(0)); @override List get $columns => [ id, @@ -366,7 +365,7 @@ class $UserEntityTable extends i3.UserEntity final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return i1.UserEntityData( id: attachedDatabase.typeMapping - .read(i0.DriftSqlType.blob, data['${effectivePrefix}id'])!, + .read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!, name: attachedDatabase.typeMapping .read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!, isAdmin: attachedDatabase.typeMapping @@ -397,7 +396,7 @@ class $UserEntityTable extends i3.UserEntity class UserEntityData extends i0.DataClass implements i0.Insertable { - final i2.Uint8List id; + final String id; final String name; final bool isAdmin; final String email; @@ -417,7 +416,7 @@ class UserEntityData extends i0.DataClass @override Map toColumns(bool nullToAbsent) { final map = {}; - map['id'] = i0.Variable(id); + map['id'] = i0.Variable(id); map['name'] = i0.Variable(name); map['is_admin'] = i0.Variable(isAdmin); map['email'] = i0.Variable(email); @@ -436,7 +435,7 @@ class UserEntityData extends i0.DataClass {i0.ValueSerializer? serializer}) { serializer ??= i0.driftRuntimeOptions.defaultSerializer; return UserEntityData( - id: serializer.fromJson(json['id']), + id: serializer.fromJson(json['id']), name: serializer.fromJson(json['name']), isAdmin: serializer.fromJson(json['isAdmin']), email: serializer.fromJson(json['email']), @@ -450,7 +449,7 @@ class UserEntityData extends i0.DataClass Map toJson({i0.ValueSerializer? serializer}) { serializer ??= i0.driftRuntimeOptions.defaultSerializer; return { - 'id': serializer.toJson(id), + 'id': serializer.toJson(id), 'name': serializer.toJson(name), 'isAdmin': serializer.toJson(isAdmin), 'email': serializer.toJson(email), @@ -462,7 +461,7 @@ class UserEntityData extends i0.DataClass } i1.UserEntityData copyWith( - {i2.Uint8List? id, + {String? id, String? name, bool? isAdmin, String? email, @@ -519,13 +518,13 @@ class UserEntityData extends i0.DataClass } @override - int get hashCode => Object.hash(i0.$driftBlobEquality.hash(id), name, isAdmin, - email, profileImagePath, updatedAt, quotaSizeInBytes, quotaUsageInBytes); + int get hashCode => Object.hash(id, name, isAdmin, email, profileImagePath, + updatedAt, quotaSizeInBytes, quotaUsageInBytes); @override bool operator ==(Object other) => identical(this, other) || (other is i1.UserEntityData && - i0.$driftBlobEquality.equals(other.id, this.id) && + other.id == this.id && other.name == this.name && other.isAdmin == this.isAdmin && other.email == this.email && @@ -536,7 +535,7 @@ class UserEntityData extends i0.DataClass } class UserEntityCompanion extends i0.UpdateCompanion { - final i0.Value id; + final i0.Value id; final i0.Value name; final i0.Value isAdmin; final i0.Value email; @@ -555,7 +554,7 @@ class UserEntityCompanion extends i0.UpdateCompanion { this.quotaUsageInBytes = const i0.Value.absent(), }); UserEntityCompanion.insert({ - required i2.Uint8List id, + required String id, required String name, this.isAdmin = const i0.Value.absent(), required String email, @@ -567,7 +566,7 @@ class UserEntityCompanion extends i0.UpdateCompanion { name = i0.Value(name), email = i0.Value(email); static i0.Insertable custom({ - i0.Expression? id, + i0.Expression? id, i0.Expression? name, i0.Expression? isAdmin, i0.Expression? email, @@ -589,7 +588,7 @@ class UserEntityCompanion extends i0.UpdateCompanion { } i1.UserEntityCompanion copyWith( - {i0.Value? id, + {i0.Value? id, i0.Value? name, i0.Value? isAdmin, i0.Value? email, @@ -613,7 +612,7 @@ class UserEntityCompanion extends i0.UpdateCompanion { Map toColumns(bool nullToAbsent) { final map = {}; if (id.present) { - map['id'] = i0.Variable(id.value); + map['id'] = i0.Variable(id.value); } if (name.present) { map['name'] = i0.Variable(name.value); diff --git a/mobile/lib/infrastructure/entities/user_metadata.entity.dart b/mobile/lib/infrastructure/entities/user_metadata.entity.dart index ebbfeebadd0..302a9ffce19 100644 --- a/mobile/lib/infrastructure/entities/user_metadata.entity.dart +++ b/mobile/lib/infrastructure/entities/user_metadata.entity.dart @@ -6,8 +6,8 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; class UserMetadataEntity extends Table with DriftDefaultsMixin { const UserMetadataEntity(); - BlobColumn get userId => - blob().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + TextColumn get userId => + text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); TextColumn get preferences => text().map(userPreferenceConverter)(); @override diff --git a/mobile/lib/infrastructure/entities/user_metadata.entity.drift.dart b/mobile/lib/infrastructure/entities/user_metadata.entity.drift.dart index 9829fd1acc0..95ab63ebf69 100644 --- a/mobile/lib/infrastructure/entities/user_metadata.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/user_metadata.entity.drift.dart @@ -3,23 +3,22 @@ import 'package:drift/drift.dart' as i0; import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart' as i1; -import 'dart:typed_data' as i2; -import 'package:immich_mobile/domain/models/user_metadata.model.dart' as i3; +import 'package:immich_mobile/domain/models/user_metadata.model.dart' as i2; import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart' - as i4; + as i3; import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart' - as i5; -import 'package:drift/internal/modular.dart' as i6; + as i4; +import 'package:drift/internal/modular.dart' as i5; typedef $$UserMetadataEntityTableCreateCompanionBuilder = i1.UserMetadataEntityCompanion Function({ - required i2.Uint8List userId, - required i3.UserPreferences preferences, + required String userId, + required i2.UserPreferences preferences, }); typedef $$UserMetadataEntityTableUpdateCompanionBuilder = i1.UserMetadataEntityCompanion Function({ - i0.Value userId, - i0.Value preferences, + i0.Value userId, + i0.Value preferences, }); final class $$UserMetadataEntityTableReferences extends i0.BaseReferences< @@ -29,26 +28,26 @@ final class $$UserMetadataEntityTableReferences extends i0.BaseReferences< $$UserMetadataEntityTableReferences( super.$_db, super.$_table, super.$_typedResult); - static i5.$UserEntityTable _userIdTable(i0.GeneratedDatabase db) => - i6.ReadDatabaseContainer(db) - .resultSet('user_entity') + static i4.$UserEntityTable _userIdTable(i0.GeneratedDatabase db) => + i5.ReadDatabaseContainer(db) + .resultSet('user_entity') .createAlias(i0.$_aliasNameGenerator( - i6.ReadDatabaseContainer(db) + i5.ReadDatabaseContainer(db) .resultSet( 'user_metadata_entity') .userId, - i6.ReadDatabaseContainer(db) - .resultSet('user_entity') + i5.ReadDatabaseContainer(db) + .resultSet('user_entity') .id)); - i5.$$UserEntityTableProcessedTableManager get userId { - final $_column = $_itemColumn('user_id')!; + i4.$$UserEntityTableProcessedTableManager get userId { + final $_column = $_itemColumn('user_id')!; - final manager = i5 + final manager = i4 .$$UserEntityTableTableManager( $_db, - i6.ReadDatabaseContainer($_db) - .resultSet('user_entity')) + i5.ReadDatabaseContainer($_db) + .resultSet('user_entity')) .filter((f) => f.id.sqlEquals($_column)); final item = $_typedResult.readTableOrNull(_userIdTable($_db)); if (item == null) return manager; @@ -66,26 +65,26 @@ class $$UserMetadataEntityTableFilterComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - i0.ColumnWithTypeConverterFilters get preferences => $composableBuilder( column: $table.preferences, builder: (column) => i0.ColumnWithTypeConverterFilters(column)); - i5.$$UserEntityTableFilterComposer get userId { - final i5.$$UserEntityTableFilterComposer composer = $composerBuilder( + i4.$$UserEntityTableFilterComposer get userId { + final i4.$$UserEntityTableFilterComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.userId, - referencedTable: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - i5.$$UserEntityTableFilterComposer( + i4.$$UserEntityTableFilterComposer( $db: $db, - $table: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -108,20 +107,20 @@ class $$UserMetadataEntityTableOrderingComposer column: $table.preferences, builder: (column) => i0.ColumnOrderings(column)); - i5.$$UserEntityTableOrderingComposer get userId { - final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder( + i4.$$UserEntityTableOrderingComposer get userId { + final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.userId, - referencedTable: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - i5.$$UserEntityTableOrderingComposer( + i4.$$UserEntityTableOrderingComposer( $db: $db, - $table: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -140,24 +139,24 @@ class $$UserMetadataEntityTableAnnotationComposer super.$addJoinBuilderToRootComposer, super.$removeJoinBuilderFromRootComposer, }); - i0.GeneratedColumnWithTypeConverter + i0.GeneratedColumnWithTypeConverter get preferences => $composableBuilder( column: $table.preferences, builder: (column) => column); - i5.$$UserEntityTableAnnotationComposer get userId { - final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder( + i4.$$UserEntityTableAnnotationComposer get userId { + final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder( composer: this, getCurrentColumn: (t) => t.userId, - referencedTable: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => - i5.$$UserEntityTableAnnotationComposer( + i4.$$UserEntityTableAnnotationComposer( $db: $db, - $table: i6.ReadDatabaseContainer($db) - .resultSet('user_entity'), + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, joinBuilder: joinBuilder, $removeJoinBuilderFromRootComposer: @@ -193,16 +192,16 @@ class $$UserMetadataEntityTableTableManager extends i0.RootTableManager< i1.$$UserMetadataEntityTableAnnotationComposer( $db: db, $table: table), updateCompanionCallback: ({ - i0.Value userId = const i0.Value.absent(), - i0.Value preferences = const i0.Value.absent(), + i0.Value userId = const i0.Value.absent(), + i0.Value preferences = const i0.Value.absent(), }) => i1.UserMetadataEntityCompanion( userId: userId, preferences: preferences, ), createCompanionCallback: ({ - required i2.Uint8List userId, - required i3.UserPreferences preferences, + required String userId, + required i2.UserPreferences preferences, }) => i1.UserMetadataEntityCompanion.insert( userId: userId, @@ -267,7 +266,7 @@ typedef $$UserMetadataEntityTableProcessedTableManager i1.UserMetadataEntityData, i0.PrefetchHooks Function({bool userId})>; -class $UserMetadataEntityTable extends i4.UserMetadataEntity +class $UserMetadataEntityTable extends i3.UserMetadataEntity with i0.TableInfo<$UserMetadataEntityTable, i1.UserMetadataEntityData> { @override final i0.GeneratedDatabase attachedDatabase; @@ -276,18 +275,18 @@ class $UserMetadataEntityTable extends i4.UserMetadataEntity static const i0.VerificationMeta _userIdMeta = const i0.VerificationMeta('userId'); @override - late final i0.GeneratedColumn userId = - i0.GeneratedColumn('user_id', aliasedName, false, - type: i0.DriftSqlType.blob, - requiredDuringInsert: true, - defaultConstraints: i0.GeneratedColumn.constraintIsAlways( - 'REFERENCES user_entity (id) ON DELETE CASCADE')); + late final i0.GeneratedColumn userId = i0.GeneratedColumn( + 'user_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); @override - late final i0.GeneratedColumnWithTypeConverter + late final i0.GeneratedColumnWithTypeConverter preferences = i0.GeneratedColumn( 'preferences', aliasedName, false, type: i0.DriftSqlType.string, requiredDuringInsert: true) - .withConverter( + .withConverter( i1.$UserMetadataEntityTable.$converterpreferences); @override List get $columns => [userId, preferences]; @@ -319,7 +318,7 @@ class $UserMetadataEntityTable extends i4.UserMetadataEntity final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return i1.UserMetadataEntityData( userId: attachedDatabase.typeMapping - .read(i0.DriftSqlType.blob, data['${effectivePrefix}user_id'])!, + .read(i0.DriftSqlType.string, data['${effectivePrefix}user_id'])!, preferences: i1.$UserMetadataEntityTable.$converterpreferences.fromSql( attachedDatabase.typeMapping.read( i0.DriftSqlType.string, data['${effectivePrefix}preferences'])!), @@ -331,8 +330,8 @@ class $UserMetadataEntityTable extends i4.UserMetadataEntity return $UserMetadataEntityTable(attachedDatabase, alias); } - static i0.JsonTypeConverter2 - $converterpreferences = i4.userPreferenceConverter; + static i0.JsonTypeConverter2 + $converterpreferences = i3.userPreferenceConverter; @override bool get withoutRowId => true; @override @@ -341,14 +340,14 @@ class $UserMetadataEntityTable extends i4.UserMetadataEntity class UserMetadataEntityData extends i0.DataClass implements i0.Insertable { - final i2.Uint8List userId; - final i3.UserPreferences preferences; + final String userId; + final i2.UserPreferences preferences; const UserMetadataEntityData( {required this.userId, required this.preferences}); @override Map toColumns(bool nullToAbsent) { final map = {}; - map['user_id'] = i0.Variable(userId); + map['user_id'] = i0.Variable(userId); { map['preferences'] = i0.Variable( i1.$UserMetadataEntityTable.$converterpreferences.toSql(preferences)); @@ -360,7 +359,7 @@ class UserMetadataEntityData extends i0.DataClass {i0.ValueSerializer? serializer}) { serializer ??= i0.driftRuntimeOptions.defaultSerializer; return UserMetadataEntityData( - userId: serializer.fromJson(json['userId']), + userId: serializer.fromJson(json['userId']), preferences: i1.$UserMetadataEntityTable.$converterpreferences .fromJson(serializer.fromJson(json['preferences'])), ); @@ -369,7 +368,7 @@ class UserMetadataEntityData extends i0.DataClass Map toJson({i0.ValueSerializer? serializer}) { serializer ??= i0.driftRuntimeOptions.defaultSerializer; return { - 'userId': serializer.toJson(userId), + 'userId': serializer.toJson(userId), 'preferences': serializer.toJson(i1 .$UserMetadataEntityTable.$converterpreferences .toJson(preferences)), @@ -377,7 +376,7 @@ class UserMetadataEntityData extends i0.DataClass } i1.UserMetadataEntityData copyWith( - {i2.Uint8List? userId, i3.UserPreferences? preferences}) => + {String? userId, i2.UserPreferences? preferences}) => i1.UserMetadataEntityData( userId: userId ?? this.userId, preferences: preferences ?? this.preferences, @@ -401,31 +400,30 @@ class UserMetadataEntityData extends i0.DataClass } @override - int get hashCode => - Object.hash(i0.$driftBlobEquality.hash(userId), preferences); + int get hashCode => Object.hash(userId, preferences); @override bool operator ==(Object other) => identical(this, other) || (other is i1.UserMetadataEntityData && - i0.$driftBlobEquality.equals(other.userId, this.userId) && + other.userId == this.userId && other.preferences == this.preferences); } class UserMetadataEntityCompanion extends i0.UpdateCompanion { - final i0.Value userId; - final i0.Value preferences; + final i0.Value userId; + final i0.Value preferences; const UserMetadataEntityCompanion({ this.userId = const i0.Value.absent(), this.preferences = const i0.Value.absent(), }); UserMetadataEntityCompanion.insert({ - required i2.Uint8List userId, - required i3.UserPreferences preferences, + required String userId, + required i2.UserPreferences preferences, }) : userId = i0.Value(userId), preferences = i0.Value(preferences); static i0.Insertable custom({ - i0.Expression? userId, + i0.Expression? userId, i0.Expression? preferences, }) { return i0.RawValuesInsertable({ @@ -435,8 +433,7 @@ class UserMetadataEntityCompanion } i1.UserMetadataEntityCompanion copyWith( - {i0.Value? userId, - i0.Value? preferences}) { + {i0.Value? userId, i0.Value? preferences}) { return i1.UserMetadataEntityCompanion( userId: userId ?? this.userId, preferences: preferences ?? this.preferences, @@ -447,7 +444,7 @@ class UserMetadataEntityCompanion Map toColumns(bool nullToAbsent) { final map = {}; if (userId.present) { - map['user_id'] = i0.Variable(userId.value); + map['user_id'] = i0.Variable(userId.value); } if (preferences.present) { map['preferences'] = i0.Variable(i1 diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index 17fcad76bf5..4ad60276a22 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -3,10 +3,12 @@ import 'dart:async'; import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart'; import 'package:immich_mobile/domain/interfaces/db.interface.dart'; +import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart'; import 'package:isar/isar.dart'; @@ -36,6 +38,8 @@ class IsarDatabaseRepository implements IDatabaseRepository { LocalAlbumEntity, LocalAssetEntity, LocalAlbumAssetEntity, + RemoteAssetEntity, + RemoteExifEntity, ], ) class Drift extends $Drift implements IDatabaseRepository { diff --git a/mobile/lib/infrastructure/repositories/db.repository.drift.dart b/mobile/lib/infrastructure/repositories/db.repository.drift.dart index 6611eb5c929..d1bda93653f 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.drift.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.drift.dart @@ -13,6 +13,10 @@ import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.d as i5; import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart' as i6; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart' + as i7; +import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart' + as i8; abstract class $Drift extends i0.GeneratedDatabase { $Drift(i0.QueryExecutor e) : super(e); @@ -28,6 +32,10 @@ abstract class $Drift extends i0.GeneratedDatabase { i5.$LocalAssetEntityTable(this); late final i6.$LocalAlbumAssetEntityTable localAlbumAssetEntity = i6.$LocalAlbumAssetEntityTable(this); + late final i7.$RemoteAssetEntityTable remoteAssetEntity = + i7.$RemoteAssetEntityTable(this); + late final i8.$RemoteExifEntityTable remoteExifEntity = + i8.$RemoteExifEntityTable(this); @override Iterable> get allTables => allSchemaEntities.whereType>(); @@ -39,7 +47,10 @@ abstract class $Drift extends i0.GeneratedDatabase { localAlbumEntity, localAssetEntity, localAlbumAssetEntity, - i5.localAssetChecksum + remoteAssetEntity, + remoteExifEntity, + i5.idxLocalAssetChecksum, + i7.uQRemoteAssetOwnerChecksum ]; @override i0.StreamQueryUpdateRules get streamUpdateRules => @@ -83,6 +94,20 @@ abstract class $Drift extends i0.GeneratedDatabase { kind: i0.UpdateKind.delete), ], ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('user_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('remote_asset_entity', kind: i0.UpdateKind.delete), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('remote_asset_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('remote_exif_entity', kind: i0.UpdateKind.delete), + ], + ), ], ); @override @@ -105,4 +130,8 @@ class $DriftManager { i5.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity); i6.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i6 .$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity); + i7.$$RemoteAssetEntityTableTableManager get remoteAssetEntity => + i7.$$RemoteAssetEntityTableTableManager(_db, _db.remoteAssetEntity); + i8.$$RemoteExifEntityTableTableManager get remoteExifEntity => + i8.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity); } diff --git a/mobile/lib/infrastructure/repositories/sync_api.repository.dart b/mobile/lib/infrastructure/repositories/sync_api.repository.dart index c69122335ec..2349f35df72 100644 --- a/mobile/lib/infrastructure/repositories/sync_api.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_api.repository.dart @@ -5,6 +5,7 @@ import 'package:http/http.dart' as http; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart'; import 'package:immich_mobile/domain/models/sync_event.model.dart'; +import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; @@ -105,6 +106,7 @@ class SyncApiRepository implements ISyncApiRepository { stopwatch.stop(); _logger .info("Remote Sync completed in ${stopwatch.elapsed.inMilliseconds}ms"); + DLog.log("Remote Sync completed in ${stopwatch.elapsed.inMilliseconds}ms"); } List _parseLines(List lines) { diff --git a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart index 5ad9a369df6..804f66c5bbc 100644 --- a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart @@ -1,12 +1,13 @@ import 'package:drift/drift.dart'; -import 'package:flutter/foundation.dart'; import 'package:immich_mobile/domain/interfaces/sync_stream.interface.dart'; -import 'package:immich_mobile/extensions/string_extensions.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:logging/logging.dart'; -import 'package:openapi/api.dart'; +import 'package:openapi/api.dart' hide AssetVisibility; class DriftSyncStreamRepository extends DriftDatabaseRepository implements ISyncStreamRepository { @@ -22,7 +23,7 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository for (final user in data) { batch.delete( _db.userEntity, - UserEntityCompanion(id: Value(user.userId.toUuidByte())), + UserEntityCompanion(id: Value(user.userId)), ); } }); @@ -44,7 +45,7 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository batch.insert( _db.userEntity, - companion.copyWith(id: Value(user.id.toUuidByte())), + companion.copyWith(id: Value(user.id)), onConflict: DoUpdate((_) => companion), ); } @@ -63,8 +64,8 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository batch.delete( _db.partnerEntity, PartnerEntityCompanion( - sharedById: Value(partner.sharedById.toUuidByte()), - sharedWithId: Value(partner.sharedWithId.toUuidByte()), + sharedById: Value(partner.sharedById), + sharedWithId: Value(partner.sharedWithId), ), ); } @@ -86,8 +87,8 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository batch.insert( _db.partnerEntity, companion.copyWith( - sharedById: Value(partner.sharedById.toUuidByte()), - sharedWithId: Value(partner.sharedWithId.toUuidByte()), + sharedById: Value(partner.sharedById), + sharedWithId: Value(partner.sharedWithId), ), onConflict: DoUpdate((_) => companion), ); @@ -99,36 +100,153 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository } } - // Assets - @override - Future updateAssetsV1(Iterable data) async { - debugPrint("updateAssetsV1 - ${data.length}"); - } - @override Future deleteAssetsV1(Iterable data) async { - debugPrint("deleteAssetsV1 - ${data.length}"); + try { + await _deleteAssetsV1(data); + } catch (e, s) { + _logger.severe('Error while processing deleteAssetsV1', e, s); + rethrow; + } } - // Partner Assets @override - Future updatePartnerAssetsV1(Iterable data) async { - debugPrint("updatePartnerAssetsV1 - ${data.length}"); + Future updateAssetsV1(Iterable data) async { + try { + await _updateAssetsV1(data); + } catch (e, s) { + _logger.severe('Error while processing updateAssetsV1', e, s); + rethrow; + } } @override Future deletePartnerAssetsV1(Iterable data) async { - debugPrint("deletePartnerAssetsV1 - ${data.length}"); + try { + await _deleteAssetsV1(data); + } catch (e, s) { + _logger.severe('Error while processing deletePartnerAssetsV1', e, s); + rethrow; + } + } + + @override + Future updatePartnerAssetsV1(Iterable data) async { + try { + await _updateAssetsV1(data); + } catch (e, s) { + _logger.severe('Error while processing updatePartnerAssetsV1', e, s); + rethrow; + } } - // EXIF @override Future updateAssetsExifV1(Iterable data) async { - debugPrint("updateAssetsExifV1 - ${data.length}"); + try { + await _updateAssetExifV1(data); + } catch (e, s) { + _logger.severe('Error while processing updateAssetsExifV1', e, s); + rethrow; + } } @override Future updatePartnerAssetsExifV1(Iterable data) async { - debugPrint("updatePartnerAssetsExifV1 - ${data.length}"); + try { + await _updateAssetExifV1(data); + } catch (e, s) { + _logger.severe('Error while processing updatePartnerAssetsExifV1', e, s); + rethrow; + } } + + Future _updateAssetsV1(Iterable data) => + _db.batch((batch) { + for (final asset in data) { + final companion = RemoteAssetEntityCompanion( + name: Value(asset.originalFileName), + type: Value(asset.type.toAssetType()), + createdAt: Value.absentIfNull(asset.fileCreatedAt), + updatedAt: Value.absentIfNull(asset.fileModifiedAt), + durationInSeconds: const Value(0), + checksum: Value(asset.checksum), + isFavorite: Value(asset.isFavorite), + ownerId: Value(asset.ownerId), + localDateTime: Value(asset.localDateTime), + thumbHash: Value(asset.thumbhash), + deletedAt: Value(asset.deletedAt), + visibility: Value(asset.visibility.toAssetVisibility()), + ); + + batch.insert( + _db.remoteAssetEntity, + companion.copyWith(id: Value(asset.id)), + onConflict: DoUpdate((_) => companion), + ); + } + }); + + Future _deleteAssetsV1(Iterable assets) => + _db.batch((batch) { + for (final asset in assets) { + batch.delete( + _db.remoteAssetEntity, + RemoteAssetEntityCompanion(id: Value(asset.assetId)), + ); + } + }); + + Future _updateAssetExifV1(Iterable data) => + _db.batch((batch) { + for (final exif in data) { + final companion = RemoteExifEntityCompanion( + city: Value(exif.city), + state: Value(exif.state), + country: Value(exif.country), + dateTimeOriginal: Value(exif.dateTimeOriginal), + description: Value(exif.description), + height: Value(exif.exifImageHeight), + width: Value(exif.exifImageWidth), + exposureTime: Value(exif.exposureTime), + fNumber: Value(exif.fNumber), + fileSize: Value(exif.fileSizeInByte), + focalLength: Value(exif.focalLength), + latitude: Value(exif.latitude), + longitude: Value(exif.longitude), + iso: Value(exif.iso), + make: Value(exif.make), + model: Value(exif.model), + orientation: Value(exif.orientation), + timeZone: Value(exif.timeZone), + rating: Value(exif.rating), + projectionType: Value(exif.projectionType), + ); + + batch.insert( + _db.remoteExifEntity, + companion.copyWith(assetId: Value(exif.assetId)), + onConflict: DoUpdate((_) => companion), + ); + } + }); +} + +extension on SyncAssetV1TypeEnum { + AssetType toAssetType() => switch (this) { + SyncAssetV1TypeEnum.IMAGE => AssetType.image, + SyncAssetV1TypeEnum.VIDEO => AssetType.video, + SyncAssetV1TypeEnum.AUDIO => AssetType.audio, + SyncAssetV1TypeEnum.OTHER => AssetType.other, + _ => throw Exception('Unknown SyncAssetV1TypeEnum value: $this'), + }; +} + +extension on SyncAssetV1VisibilityEnum { + AssetVisibility toAssetVisibility() => switch (this) { + SyncAssetV1VisibilityEnum.timeline => AssetVisibility.timeline, + SyncAssetV1VisibilityEnum.hidden => AssetVisibility.hidden, + SyncAssetV1VisibilityEnum.archive => AssetVisibility.archive, + SyncAssetV1VisibilityEnum.locked => AssetVisibility.locked, + _ => throw Exception('Unknown SyncAssetV1VisibilityEnum value: $this'), + }; } diff --git a/mobile/lib/presentation/pages/dev/feat_in_development.page.dart b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart index da0bea157fe..3ff0b12b959 100644 --- a/mobile/lib/presentation/pages/dev/feat_in_development.page.dart +++ b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart @@ -53,11 +53,38 @@ final _features = [ await db.localAlbumAssetEntity.deleteAll(); }, ), + _Feature( + name: 'Clear Remote Data', + icon: Icons.delete_sweep_rounded, + onTap: (_, ref) async { + final db = ref.read(driftProvider); + await db.remoteAssetEntity.deleteAll(); + await db.remoteExifEntity.deleteAll(); + }, + ), _Feature( name: 'Local Media Summary', icon: Icons.table_chart_rounded, onTap: (ctx, _) => ctx.pushRoute(const LocalMediaSummaryRoute()), ), + _Feature( + name: 'Remote Media Summary', + icon: Icons.summarize_rounded, + onTap: (ctx, _) => ctx.pushRoute(const RemoteMediaSummaryRoute()), + ), + _Feature( + name: 'Reset Sqlite', + icon: Icons.table_view_rounded, + onTap: (_, ref) async { + final drift = ref.read(driftProvider); + // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member + final migrator = drift.createMigrator(); + for (final entity in drift.allSchemaEntities) { + await migrator.drop(entity); + await migrator.create(entity); + } + }, + ), ]; @RoutePage() diff --git a/mobile/lib/presentation/pages/dev/local_media_stat.page.dart b/mobile/lib/presentation/pages/dev/media_stat.page.dart similarity index 76% rename from mobile/lib/presentation/pages/dev/local_media_stat.page.dart rename to mobile/lib/presentation/pages/dev/media_stat.page.dart index b42cae84fed..5debeff31d8 100644 --- a/mobile/lib/presentation/pages/dev/local_media_stat.page.dart +++ b/mobile/lib/presentation/pages/dev/media_stat.page.dart @@ -1,3 +1,5 @@ +// ignore_for_file: prefer-single-widget-per-file + import 'package:auto_route/auto_route.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -8,7 +10,40 @@ import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -final _stats = [ +class _Stat { + const _Stat({required this.name, required this.load}); + + final String name; + final Future Function(Drift _) load; +} + +class _Summary extends StatelessWidget { + final String name; + final Future countFuture; + + const _Summary({required this.name, required this.countFuture}); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: countFuture, + builder: (ctx, snapshot) { + final Widget subtitle; + + if (snapshot.connectionState == ConnectionState.waiting) { + subtitle = const CircularProgressIndicator(); + } else if (snapshot.hasError) { + subtitle = const Icon(Icons.error_rounded); + } else { + subtitle = Text('${snapshot.data ?? 0}'); + } + return ListTile(title: Text(name), trailing: subtitle); + }, + ); + } +} + +final _localStats = [ _Stat( name: 'Local Assets', load: (db) => db.managers.localAssetEntity.count(), @@ -36,11 +71,11 @@ class LocalMediaSummaryPage extends StatelessWidget { slivers: [ SliverList.builder( itemBuilder: (_, index) { - final stat = _stats[index]; + final stat = _localStats[index]; final countFuture = stat.load(db); return _Summary(name: stat.name, countFuture: countFuture); }, - itemCount: _stats.length, + itemCount: _localStats.length, ), SliverToBoxAdapter( child: Column( @@ -90,36 +125,43 @@ class LocalMediaSummaryPage extends StatelessWidget { } } -// ignore: prefer-single-widget-per-file -class _Summary extends StatelessWidget { - final String name; - final Future countFuture; +final _remoteStats = [ + _Stat( + name: 'Remote Assets', + load: (db) => db.managers.remoteAssetEntity.count(), + ), + _Stat( + name: 'Exif Entities', + load: (db) => db.managers.remoteExifEntity.count(), + ), +]; - const _Summary({required this.name, required this.countFuture}); +@RoutePage() +class RemoteMediaSummaryPage extends StatelessWidget { + const RemoteMediaSummaryPage({super.key}); @override Widget build(BuildContext context) { - return FutureBuilder( - future: countFuture, - builder: (ctx, snapshot) { - final Widget subtitle; + return Scaffold( + appBar: AppBar(title: const Text('Remote Media Summary')), + body: Consumer( + builder: (ctx, ref, __) { + final db = ref.watch(driftProvider); - if (snapshot.connectionState == ConnectionState.waiting) { - subtitle = const CircularProgressIndicator(); - } else if (snapshot.hasError) { - subtitle = const Icon(Icons.error_rounded); - } else { - subtitle = Text('${snapshot.data ?? 0}'); - } - return ListTile(title: Text(name), trailing: subtitle); - }, + return CustomScrollView( + slivers: [ + SliverList.builder( + itemBuilder: (_, index) { + final stat = _remoteStats[index]; + final countFuture = stat.load(db); + return _Summary(name: stat.name, countFuture: countFuture); + }, + itemCount: _remoteStats.length, + ), + ], + ); + }, + ), ); } } - -class _Stat { - const _Stat({required this.name, required this.load}); - - final String name; - final Future Function(Drift _) load; -} diff --git a/mobile/lib/repositories/auth.repository.dart b/mobile/lib/repositories/auth.repository.dart index f9e82e1635a..01d2684faf5 100644 --- a/mobile/lib/repositories/auth.repository.dart +++ b/mobile/lib/repositories/auth.repository.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:drift/drift.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/album.entity.dart'; @@ -8,17 +9,22 @@ import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/interfaces/auth.interface.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/repositories/database.repository.dart'; final authRepositoryProvider = Provider( - (ref) => AuthRepository(ref.watch(dbProvider)), + (ref) => + AuthRepository(ref.watch(dbProvider), drift: ref.watch(driftProvider)), ); class AuthRepository extends DatabaseRepository implements IAuthRepository { - AuthRepository(super.db); + final Drift _drift; + + AuthRepository(super.db, {required Drift drift}) : _drift = drift; @override Future clearLocalData() { @@ -29,6 +35,8 @@ class AuthRepository extends DatabaseRepository implements IAuthRepository { db.albums.clear(), db.eTags.clear(), db.users.clear(), + _drift.remoteAssetEntity.deleteAll(), + _drift.remoteExifEntity.deleteAll(), ]); }); } diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index a6e1d89ff38..1f14aaa5bfb 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -64,7 +64,7 @@ import 'package:immich_mobile/pages/search/recently_taken.page.dart'; import 'package:immich_mobile/pages/search/search.page.dart'; import 'package:immich_mobile/pages/share_intent/share_intent.page.dart'; import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart'; -import 'package:immich_mobile/presentation/pages/dev/local_media_stat.page.dart'; +import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/routing/auth_guard.dart'; @@ -326,5 +326,9 @@ class AppRouter extends RootStackRouter { page: LocalMediaSummaryRoute.page, guards: [_authGuard, _duplicateGuard], ), + AutoRoute( + page: RemoteMediaSummaryRoute.page, + guards: [_authGuard, _duplicateGuard], + ), ]; } diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 57fb8cef809..0c57949f04c 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -1356,6 +1356,22 @@ class RecentlyTakenRoute extends PageRouteInfo { ); } +/// generated route for +/// [RemoteMediaSummaryPage] +class RemoteMediaSummaryRoute extends PageRouteInfo { + const RemoteMediaSummaryRoute({List? children}) + : super(RemoteMediaSummaryRoute.name, initialChildren: children); + + static const String name = 'RemoteMediaSummaryRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const RemoteMediaSummaryPage(); + }, + ); +} + /// generated route for /// [SearchPage] class SearchRoute extends PageRouteInfo { diff --git a/mobile/makefile b/mobile/makefile index b797a659283..ec0d08f0878 100644 --- a/mobile/makefile +++ b/mobile/makefile @@ -21,7 +21,7 @@ create_splash: build_release_android: flutter build appbundle -migrations: +migration: dart run drift_dev make-migrations translation:
- +