From b3e51cc8492ceedec92f5087f8c25b5543e6c77b Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 20 Nov 2022 11:43:10 -0600 Subject: [PATCH 01/30] feat(mobile) Add OAuth Login On Mobile (#990) * Added return type for oauth/callback * Remove console.log * Redirect app * Wording * Added loading state change * Added OAuth login on mobile * Return correct status for correct redirection * Auto discovery OAuth Login --- docs/docs/usage/img/authentik-redirect.png | Bin 0 -> 52213 bytes docs/docs/usage/oauth.md | 12 +- mobile/android/app/build.gradle | 2 +- .../android/app/src/main/AndroidManifest.xml | 19 +- mobile/assets/i18n/en-US.json | 4 +- mobile/ios/Podfile.lock | 6 + .../models/hive_saved_login_info.model.dart | 10 +- .../models/hive_saved_login_info.model.g.dart | Bin 1359 -> 1504 bytes .../providers/authentication.provider.dart | 133 +++++------ .../login/providers/oauth.provider.dart | 6 + .../modules/login/services/oauth.service.dart | 39 ++++ mobile/lib/modules/login/ui/login_form.dart | 209 ++++++++++++++++-- mobile/lib/shared/services/api.service.dart | 2 + mobile/lib/shared/ui/immich_toast.dart | 3 +- mobile/lib/shared/views/splash_screen.dart | 30 +-- .../openapi/lib/model/user_response_dto.dart | Bin 5587 -> 5742 bytes mobile/pubspec.lock | 7 + mobile/pubspec.yaml | 1 + .../src/api-v1/oauth/oauth.controller.ts | 6 +- 19 files changed, 384 insertions(+), 105 deletions(-) create mode 100644 docs/docs/usage/img/authentik-redirect.png create mode 100644 mobile/lib/modules/login/providers/oauth.provider.dart create mode 100644 mobile/lib/modules/login/services/oauth.service.dart diff --git a/docs/docs/usage/img/authentik-redirect.png b/docs/docs/usage/img/authentik-redirect.png new file mode 100644 index 0000000000000000000000000000000000000000..c16c03bab5cb8310a291f7cce5b5a627fad1675c GIT binary patch literal 52213 zcmeFZWmHse7e0IlDFsmxq@+|pQaU75N>V_j1O(}!Qy5|t38h0i1qmgjrB$S1hLRD6 zkQ|2Y0frgggZ_M;=eOSX%m3@U{%0)~4CkCX_TKm2*WUMa&+7-;s?=0WQ~&@_-&a%8 z0{~JO01!`7kb(bsXm{m4_>0IxPxUTP24}&5A4qNQXx#yTikP#<7H7cEl&)$<9sodp z{q%?EzTUM>0O);lU+K=n=Vq(;AkRk&nb;^>0+irSuqZwDt)hp>-P;N*h}_xL=)g>- zmzb!gpi=FVb_ESpQ#57O3f>fiYvvpFJ$N5?lPu`MC(hiPax57F|# zdHz1Z_2-G{0PLhDMSdPkjFgJ`j1us>ujEUAZi)j(!`TF@FhxS~)33&g1pB9l%fJ~D z_Pf73`8%T9(ESotp!ln<;$gvyGb?~E5p5pqbQJ8=e};V9V=+>Wsw-uyN;z?QN~(Bz$5mPpcoiOAytRuNPSrh~S@VUybTax9lD~ud zXKqj^R5TBEJp4t|3-*rc&Lw~dd|Ppx{cpR`#M=cg4oTR9SaY6o*!?3H*?(s3i_X*p z=CjG@`AMDpE?ps_J?%Q5k(gacNi&KG@E!j0j+nii*3LtX;_r@t zls^-AzC2RgMD$?u(dNI0KS&4IbJ$VCUY5V)28hANVZD@pS|XgN6x)Tm*{e0 zUt`dKoO?3-Xu2#e#HTz#02kBy<+=~9H;HHrDP~~f-`1?4WxBo{H*o!<2o_c89T7QP zSfsSuvyUpTY(bPIj%nf4nR?ExnqT)2MV#DRNgx7X?0=;tcu$a~Yb!YHJ@yiI<>$Gs zeG`9HpS`5DImpPDFK%N~Vg_Zq=Jyiy9js=?1Kb6@?hGh1N(C5t*Q_O&`Mgf`sC+-_ zqd);uhi}!0hHgYHeA^!i2+M?|ugEfh#s&LF_oRx+3=HSX;u%wErr$nBix?qgrHJju zrXtqUw$T&4eEBwSko&I!pXEBn(LL$%6_E3!7Y30bk#u--_=6uCNYNU-W9dlf-h}}%)PM63?$AOEbtj#oq;~y{03{CJX7V6oeegW9`_>Z}do66B}0L14(qlXuYt8d zd3|6RzZpvW_UzLMa>X#~{u)B9R-CmEgG3gmE#06LjQ|DgOE9=h*cGf<0ZdWa7VUA$ z2z$8ho$&BHKB5O{()^GDoXq$w)23nS`@4@YxVqks#!F4&A0vr#UhIb$Iel!Y)1VSE znoMVXrJ2IXx39{gHCNU+HGc%+1mVj^ZCZbhz;-Vq#G+c$BT46nSn`Xtsj{rZwa`eX`JK4BmBz; zvR&Wz6`Arw#?SXEj+iWHBD2UFo%$oJ%TA1{*0uTtdad%jT0UW-7Xdp(Kl4!;L-8?- ziXZ!4LfGhp26F&t&vPVUXWiSuXnK4WF&nvX$B(sO<(|^_Sq&i3hM09Ng=vL2bBc54 zS@WHbPl$8O#ttSq>sGp$iucV|-kC~g&O~giCwN8Jbe`)gQ0s4AWD;6n0ru9PB%z%g zv;~SbF(KU~IbK38aqUKV^)I#K#$s6D6TQbyO?r!Eax51q$jT0<)`s?A#o?b35QbiF z7vo>tVb-Q=Zx`2|6#7F`SEs&=cNmwdabj#%l-`^RauTN^eALNx<)%QAsgc|=(O3;n!;Yi7 zivD_6ekC=mK=bw1IVo);+Q-js+5>dcjORVOw=&*MTWH4}Iu+29l}VTBFZS+V8m&<# zdn8|OdfhbpbK&kKQJ~?u<+HvA-amP#O}N4W5RUsEHUYJ6F_Q3=r$oTDTz--qosPq< z?3`>PWSd$}uSq7&!Q_dg@o-@H{SoxiTa1BK;QlTFe|dZ7WEvTXTJ&g=uaX1T#tfp- zssi;QeS7T@I6awE*45AL72bI)3ZHN2-}Y6v$=7J&wf2x?O$yMOLRHyxeYls$Mw|(C z4t$(0GCZ!Lwh_uS?N(@rTrAsM8e5v$;j``l4aXEEIOda&^ld->BE8bG9xtHvmDlS> zgxtk7vC*~bAuyc<2xYLZI(SJq6ChgM2Q2%FTC6#4soA$Q2DK3=ttfz0T(@?%79Hpe0KECD#5q}zP+1{+z4BhqWcZ5-PTX=IBmaexA^ND%-FVx8vav}t!8jee&;Uv{Tq8J1^5nQ;+$RD zb8>Ic{$tMAUBRA~My-_{!^Cf2yQPj{LD!-sZ`#i<@$j6~ENCY)$Ig+*s3M(pn^O8@QwnUW|U1(B)>w=?5YIdBC zj&Gk`+r0?n7Di{>3Lpd~M3=qx(4_X%#D(4mlmM~ObB384Gw?CG`sMUNFST#YL*vNl zue${@?dM=EZjVNbf_cx+s{ro{z(AH|NTjYw{8h;rpiBPJ+!Xk1)Uy~|GiBmuWCC{^ zjuF~X7O%91fFk3^O=+Uq9bvgs`t!T_U({+dVsB#2LXo1CWm3Z8BIXraPu;dN2JL-` zb0TmCR+$K~5aDDqjGs96W@UB=qc2J3ubNTE@CH!;z~qz==Tt`%w*1RAWi_SviD;9? z^+*+uo}@)z66VLLM-d#=P)Uv+p?9!*MpaPv;s~^nQkiTr>?X}7- zj((sSFLzcwxuna{g%RxwCI;xlM?ij_l_qOcbrI6G!I__p6832%i4zcEZOfMrE5K`A z1TFw?&oU@%b&!yP5o<4uX;d|X!|a0H$F}7&@}uxW)fTr}2p+ zk)Ppj+p%R;E8py*GK=8Y&onS%qE0(f#rqouyZiUcrCsH?Cgr!m*m3h@so?^daCg?3 z_Xf2SV*Y_(RQ}Au^AYG8=~hc`x=_+|9r&havI_9Pw{GtpO0c$FBcdgGaCmDQ;AxYB zQXi*Wevw3kaiF31mGFH(;@>!q^?E~`<3kC1$9M;z`&`ErP?kWhc;@zb0GP|Dq=Y5m zwfxNRSFd})dQ6Df`D@x~VDTf`&q!&B1`cdMN8Rq>ey|`=xxOy-BGfkHlO^4i1mu_O zUd^u>f)|CH@_&o(|mPMb0b-2aiw{qB$J%B`9?R$S0+cyQ0 zjdQXVZ%Te{D2&5ajpt2HCcWHmBR>ApHz7>}x=gQjP!Q*QX~{Du2?ybcAc#6ujEHl# zlK=`0CthN9@nuH7`TFYsCva0ilV|{3b|`1vSxEoz+o6lVn2SKwwACUeD9=d5Px4T< zCrLV2+1qU@LsFJ8eA%De9FraN;XKiUty&>8U{^9ttO8=UWVT{z*zVhHrrPKGMwypQ z30vp5Uu}Kt^~`j>^aLp_Cqt|D;p(s_-tg*g$${4BAjcjMx+nc(ZmIQXHMO>%`?tO? zEZJ$NDjbIjCQsEKO1tc<>i||>$))h!$ddj3Q;4UTUeD7hNxB|4BTu&B)gKynM)51g zMt^%$SVnNAjBQFj5UAGNSH7A86g3gEue_dU|J4DvDKzrL`(0aT7& z7jW=j5J24v4y)jloKNEv*Qo2-@|PZ{7c@%tz#H^@Lu+EUG91l+p`4FI#`bJ|-oEx| zv}&)zmw!0#-BOAWTq%WI`1<4eiifdCTV1L3Do~^{n(1nAg#)-*2k;aI9NnI76#97e z5&hJT1%=>ging6FenGLd=_c;PkAVqz+hA&Fb}GZ`z)cXg+TUo*lV(}98vZfAGN9~; zb{!li+=MoMEPKzWVHpi(L?Qa+` z)@E}=_=Lu+QjW5u3BOmNFPjuJ<%Ual%KJQNFq`6pxKk;9tk2w-(yAgs8M5M0$nJfv z=G5AC2b1d_tuxad;+#Odfq|6^bj@n>Cg$MvzX0GKz(o96??$S z%`c)4K}EscmFNObe&p0RziE#)T9>QTsVz6<5htvbnR4L#zk=%?PzES=@x_Lo@*sfQrg=RAGuu|^bPKMljhc|se zf;ahZ3c;J3I2%PXzPs=zMs;peGcLc4k~~!1Qgr z$ux$%(raJNNZS`s8m_?8Uj#7nATD+aUdCT3WmiMw&4d@T>MqwL-u%?#lf6qgs6$H3 zLqm_3MUCwm6Ee7aiZ(7$f6~Go{D8WQOZHg?#P}}bMOizoHh0uHi5?u)+r_Lt=%4cn zMVc%YlP8Gx-#4vzR0BV>y=&1>=-wCyJ?WG?Gz6?wb*-r=&2@b;rVxy zFCzZ#Pb3!8Lv3r^jBxT}OB*SwXB6p~=9NP%NEN4KdPS&Vj$|e1eko&t9CIoyzK~aj(=vM9=U*Sp8q?3EWo4v<>Gtq`DR2YP~@uBxu1Rw7{XnUJfKXgPRLdY z(59jceCc;57Q+&fb`7|>%e2c9@(Qe_M7wR=8=4~%@CC#UiqD<%w^}>d;=!f*&k$xq zXWiDNgXA)|#EE$#B1qhi7osXH2PeLu1~TgM1)89lj?zE*80N?nh~TUfoFAQw;~1wm z*?hn}u2CbpQQtM*vt{o&q@Dt6#_!R(b~d=>);$MjqZ`cT@`5BBh+Ds{R-GQO zSG_OGLQpu`I}gQ=k6hqI=Dg1!$+7h8c!Yl+r;65_H=~mQ9UPZO)H27_6y*{lYU71e22D}*_`=PLl&wNOnV@3Z_A5)yIn(^!+ zkSflKa>>|uZ5dc_Ulym^VGqq+Mn8gm|}Y^xrFO)nO3W9^rKDv)cgh3Boi$T;&x;U>(=h#thF3k_#t#_xd< z2)v%=+Y!YQh)&9WaBM=BU7veN9iv!FTJDaXIJqs4JukQgyt!16IG)d1Bjt1hxS1Ie zO{{)Yg29g>l*)(oNs6w`gk8-W*pF5Q@{u+U8Uxp`>^)m_z;)lic!FlPto$!&> zb!``8o68D$BpcVzG(h7q1UJrz{$)s}=;z$(7R@}N3hJdikfvZxx!7s&88kPX@DVH8 zZOc&TbYm{-UR^I%)2?wYl)WJnbLCLeUFY^5?n_Y6o=kk8%X6GcapzvvC2@FF4(b4_ zn+JcS4Pwa+Uqrssn`ftkwm2<&0v*iCl(1?Ky)L%BuhEZrWk|Th@7vyp_AA#!ciV1W zRV~Jfn|D4$uHYrs*sNRNfdf+5@>r;z9a=1#2}p@X8!fpdOs)81ubIordC3kFbUU0N zL$)02q8YoN9X4(6CpI{ZNbf?q*0*L!74OXX>s)IlU$_Xg*GnMoX7x+$)BY)nUg1>- z=vIZ(YH!&u+g%>Yz4`vWjF=n081P2+C8s^N(0yA2t?^q?9w!vA$|xLT$Kk}TeCmGG zkY`5D{cIA16vqP&C++QXorjEzq`l=J;Urr2)F5 zbI=ZH)gd&$Gmx0S<`!+#)f~Pf7q@%VDFX~;a~#%UHIFoJ)ch@ws_7E52dNgvDy*wk zdBl!LW!7AyAF_xXws@?C>kxNVjH`aegeF{e*NOMkPLLWs#reEhZZ8}2*ZO_%?FWPr zZ(NW~iYC+JdD*PAU9<0peet#WTA}}A?{NHAaz_+7`K;fca(|4adBiIa2Jn~{ zKG7PtILB0|Uv@)ICj+Mn%o}!9>;%}{kFl2tHOH6i8Zj0huB#4Za~-Y+r|sHmWZ2fl zW*fLgr|;q%!Fqr{%9noIpTY9{pYk>=?j5P(SM4hAO-%*Q?!SPU3IeD#d15OIzz0dp5BvXL7g1m$@havK#gu0I4UY5DeDfk#I7 z`m~@4%VeiUuci$uNBk#IFVgp{x8R%0mUO67{eW)|4Sf6Cejk|3g~;oWDlX1NhTLCQ zZ2^j65m5loQC3GyEZXnde-&U1SS|l@2ot$NYH!Q_vtm=mwxMhya@bABa_AG6Hg|O_ z&~BCCfswnl#$4d+A`@*6j@&=DxZ9FUu|059)gj)tl#VFYK_+QBKlcq28p?^m8r732 zh6J)jfB&!5o`pTsC)bk&K6sb3$=HvNy&mRO4+hf#D$J`;dzqH$Q=`p>aI7Ja(gM|U z(o@7ambH@F%&NoJ5XRux2hd+V`CCJ7H@dZO#|LJ=-J!_2yFRG4noIV33j@?KnW94r zjs1i2yi0}piDViV0F0dETKb5dr>4f`r*A*Bfss%iZkRF;4FQS{rfm#&busU$%PC zk5)vSPCtZH@ld8NB=q8)xmR=`hWdb!3fxN-USqIs0ezIMKFU%i%QhD)18)Xx6amU( z8vC>&t4c3r!z+5g3uK0aTLas+t1VQYei-?9erOn0?Sy zUIwd-?e*pD&*ADEL<2w1C_2Q`7L6bt{9Pv(h;wvnV?liL{DGbD3}YRMff`!C9Q{Om zjo}8-z^8U+mw4?U>C_EIF5spUkl*18#{Uv^M&iA{Uhp*2I}3ut(Xj#HFV#Wv2_suP z`EyC-T*VH|nz*#c;X(fDVBdOjMYr)(F|{kyl&}tYKyU80!iPY!`z`2%pMPWCE5I&M z;2L=A70-CF`gr%SwqeFIDE-Bf?-)7<=2=gQxzI-i0dvn!n_4fwS~URE`T>pGmF_RB z?H+loqi=XY^;8$`toAgp5h+B&hioI>yE$3$WQy56afKVnvO!IK+v|gSt;SXx*%Q+f zr@9BuC<0HeqUDms%4BRC*#qxE9uz*?=G$uj5_bWmt zCT>GC=Hu1g>6RP|3uSq&&TVEav>yZ_VfoXx3v>Vm4Hha4_B{AH&>MAy`9uTh+dl>N zDxXF^Y1xs(8c3dYst7uHm9e`d?Bd#;6T>Sx|ovFdQUM)V*TtA;X-to<0`J+#-L)yY@OwWfIyknXTAHXcS@Z^{}}fT>PI8J9DrWuP10p>7vgmM!g>-QV$mhHpda^*gbZm{ zbl)YRLs(W@T#L@fAIf0bKgH72Pgk6KsEJ#sdbp-FgB%#ejV?eRc=G2r-j?AsGjt{`A&2hGy;IiUcWoRu4oRsbc%*AYFPU?}O_Alf->= z^OYo#Ct$5R9Z+Lkqb;f?ie30sC?dQWS98*tw`#i`f{S229cT`1&kaCNmB}$O42+c% z`3_+pMl94*xG?!2-#gTK3#*PxyajkrgH$h6M8x&_kt)PF6=z{1*ATiK*b$d*5_W^r zR$yByMrRbcsL6ZefSWeBH>|{%irEniuS%C*pZ7;DYU$&Pz=Cnl7hRM=r0q@f&!9=w z62*Q%#9^dvoUJd<`muSBt6| zhz>|0Rh(Jg(Tzm(d0fjp&N&=c2E_J>Vr7^(X#a_9sg>Z<{;cCXz0$x6J~;4(YT z*y5iD^U2?sDuuTZP?HS}@fPeA2X1iwA?D&D`F z>`n#qG#S1{d!fSfKa*Vnx`xlbtWFB&b=+cE0n}8Fmn1Yz@BVK3>4e)>21}rCgrl_7 zFF`;}dQ^1!N3HhgngUu7G=@RTC!~sj;!n01DECa3e_damC|J92!Oe3GG+PCd=1R!0 zh|$5?N2{J!lp+i%X7oHB$SSOdH&9dY*9DZtaO!W1M=&OM%)Rn^Eo7v}k z_ZQnmu;YAdkQCt=^;F$PDc{Z`aPO#g-~8v*i+3;YWhHZsdgLGh-p7`>zmR)Ur;irWfV}L8gGxxdecx z|6JFW*4a{;LhtBOI2PQz(Ar_@Y-H$(bdh33^Ffem$m~heZCAGws#Hs_>OwmTv4E(= zo5V~@nE17H@B8IAFZwfHp#fOSo<|}l&RBUdp@hTfjM<4WliGAda+~rP(r!)EI%Kt% zQshk#qBr3-(ZKFu<+sN5NIt7A?1ZADdK5n-#-}dqwM?zdv61#+)}CYF?D}IBPjTt& zlULUpOA2|2PFKt`vVXSvbE=2<8k71y$)<}YImWehe6moCt`kULqV_20OBN`cY|lO2 z!7g!XZPaZTXE5%}0=FL-#1gsAqpdVby%PIc!naJvLJPJi&LiV>+y}k!Cq_< zeRaXF$I>ldU0gr3b)j%Jc>^^oo2P`iC*h>(bRGxj1T(GdJkQ~~Lwj-V=jdP)qOyUv zyFxWALUpwJ+X_QZ0~e*qX~j_3<}dF+ox7RYM)rA<&urz!{DWbqH*A2t)bO6e!_UKf%;j}(UwlVX9*joPz{JUf2Sq}pbAR1; zOynidRbWud?|L`q5YdijnPY>_`IN5R;1`V?U@AOaGm(scH}& z;uw>bvZZ#AUpA%1Sk2P_4KanT$pu~_vps{ z88&B*`A_!x=yA{SAEZWJ1E#d>w#^LhUR{}hub(rQ;l`x})OeMd@<6Ek^!3Ue>$_z- zv`jcjGKiqdCN(9}vx2U?%W_!rX^?^7j-4o3={28!4r;nQKEy0{h1Z~aRe8nAyvu2e z+?=GdwyRPz^D3mZv98N)uKNuwU?i10OKQ|43YV$`3#mrP!{s#1=A5TRA_G+#qXKCH z1{ewf2>vY+Eb~LK3!1E+DlZr0&jx0!>^OP2z76R6wDJ1IGnDlY0nrI2J^lF)8v+SY zVI|N6ZlGsUsOgjM1P%-YG=F716e|1HUOg+0pUh8k8e9-Eu zF8)q5pqT7f9dTF|d&nfAl_C%kpd9G4_Zj7zIn!@iY5gLUnJ>d>-E?VWv|`+-N@Auc zjSFI>GNt_q+AN*t|HdEDywd3NwlCY#Yw=`~?^^R+`XD5w*WCz~Aa}#j&wE}XxXDIC z8x+ydIdIb0gl*y%rsM|q`z6u6BG^Eok6tACF8oQR(s@m9E&-T_Qq7|!Pj4lD?v&6W z+A&8*x)>rxo8ewGy@RRLmFhuF=3mzNZsQEHHbN?vPmqzYsL?DqWW~k??z=D=+Vc z?G_E_WqD~ths$b27QP6!r_$fB;m%Kqzm#^at4x`R&=>otoJ?ip$+pA#{1gK>Z3dj@q7lmf@U7$t(oKeonT*(lqa;eDTyZ7ANZ9@dw%BWGR;qvLh=W?6j znZ7%d@0Kj`7HP^VW9mLlVvSybRl)i~{!i=oHYZ3rt@H3>3xO^+1s9BwOf2^n0=&Ok zE*!3t0D#qBP706p^O-wKMXx9(;P%?UE_$tUz}U>`!=t&fUTO+;HsHr?w+YkOs7vZv z;YLq>QmbsY#PzPNrke$ruR_0n>A#17Y&uPV3B2*l?+L$~Ts?Lit-dzMgl#Dt0}a z3oY<7$A63@iuGB)Jo6$_R_`V*RX9?(=bJ;@Pc&w4F|sP~emE;`mfm#5$Y02F^YQany{jbyRV+Yn#*;5_yYez4}1 zV>r28R=_waZDWW&&$vCqwfbX&=K2RBx-o*Fk%iM7o7cSO>Rbxz#f7t^eJc@kz}tMX z*i^9Z=swDK`m63SGq;(?xE$^ls-(d$p(hG#zfB576Y!@jqxd`jP_uXf==P!>u2;XS zk6Dcmz+po^D*rwcnnWg_p z%i>L+O9b=q=S+f0E%Qpx0dJ3-!8Vy9hjI=yBaTPt(6mt+7;jNThIqa^dk!XO zoasUxWTW6N6{I+K@o3qHi>nna;f&i8-(=RA&yqYA-!wDLH>hpAcY z22++lb+T)cu?zgCj77Eopcb)bUVx-PB3~)ZJA7#VhQQUpfc?p{)*X_b^*?+|Ou0;@ zEka})*!Oo?Am^b}dftY!nsq~SPp^d{a9C>8Q^V3)Mrmh+VsvsNwDkV?Uj79PHylnHo36egDO=uz)CLH+4RGzhb*Fx zE_J$^b2pXEdFPzbQeX@Q+v|ukO6ROX+K~TFg1}AaQy#1u;K?XFz}F}tkPtHMEjyj0 z-(tk&!B83}Kz^@bXg#rvw}rFK*R7EKOrZvKQv)2i)v9hY-K-pK!mNa(V2UkUJam5> zD&Y)8MzI6i@XMW=&;!MeGkr4fo=dllz}_MGK>>1H4~ec%aN> zZp#J+MiuHmv{w*JkrA3q>9xZH_n^NwjqD$q?T>8)S-9Qpo0#vdKUpK{$Jnkn=6~2>s6M4KtT<5O+52K;eQ|yvI?;yp8Caeu(&Kza2Ozp?=n=QSP zuOl3wD{b(Xwm%PlZI$wEsqHol?k+~W`2@^S0H)k4a4X|`dZwBX_3OT|*&{P!# zWPE-FrtXPTKey?AlgZsrgy?2iAOxj4kSig@*6-_5>?S#yxCc*svPy5VeiHKDJRgQc4|#_AeZRE1y!x#jWA0Y2cm&& zvqago&BDhL$5Y2=y?+i@7H%=63eQ0Fa3dz6d*LMR2g%r3TOhb>NeP6+u#c7gCwT)082HdCauv)5R%$!K%VcHXsR zlUtKGC-T((!)+)*N9{Huh7aT^$h!#a$@A6O-DJB;-^)kzIia21;U91 zD>`UGDD#C=Sb)Z;nalWMHH*JZ2><8tR5OVg$Rm-JQ5)Z_{GfCTte7!M(~tvNaw0$J z);dBc)vsF$E!6J$Qrusu7;`JADuZSQ zkGgJt?Cnd0MJA(x&qg$4XDxmBt44#g94X=RPant}%XDZgQ!-ZG=54W-Sqkpq1j_ zoILdb1cct>>Q(US z!JnwM#g*Ux(K>~p%OrF=iS!1NjETFWMknlT%z$dA4XXM@m(Ykuk{crsDQ+P_>a!*+ zIto=^()g;9-$6PU}$h&{w)<)~$E2fq63 z+-ltAhUS{}+S0-*WIgU$%Zc?0b0vX>nsc;=FC?0v+YH#PxGaoaj<9wc9@pHmD=*re zjdGb^^?9CW;% zD;JiQx{kZ8n6|Z-vN=9+G{wnz&v-rqcFmGYT2YhbTZ1!@%C=In`su*XDl8?n)zlc@ z`G&m&kxt{2qKUOonFwU-&5biy%1M{f325?E!|egNE$mjAyD8C2R|#GLxL@mMbAv%Bt*F z4!_6{uwIk=Mz{i(`uCTB{&=IEQjZf}Xl5y|UC&aeoKarvH)3{|59zrQLm0WrZ)QG) zO)2*Gy4+Ux4cC4Q>b!`PM~{ocaGe)0thp|g*h*b6mR=9aa%h9zI&1!>1K z4ogcaNFt$D`oiRiP5$-16V3)`kCL$Y}MbQSma4?^6j8WEP=?(nmF5^ zgUS1ANZ@}Ow`^WDIx+rb$d>iCIY#=9;oQy(#cmX0B=p7k`G=i(?u}F6xexH%2>GdJ zoGr5d?G3qr@c;zrYbJq9Wz60OBsqOc%t2k?G#aZ}uaoi;ey~lPvpI0h z@k8oDI>b833%;r=v-oRq_i^STy)JXNT#|yF-^wlO5$6P_7L3JyWsn&kbaif$S160w z40*WY(*3IZaAQUadDi(V^hc9?*}5COi--cVt=VxZvn#0?8(Z$BeA2s&l0ZW%7p+LW z-|*Pgp#Hk!&GOaQ`F+(u$W@DHzLp>9ztUu^>=mx!nF1NNn_BA(u8#b&>vyTTdtD#! zTznt4>K!tw)w?w#JYqr?13Q4`^^%;N5 zkfao*124+8HbQR}z?4I7SPT!!q*wp)622{RMiD*VetopE%e7+?AxRBCTa)BvdiB;l1{TagwNB zXF28xJn*aToiRTPgxU0N{RFT?;zK%=x9vXKmG6jHJ+7~bYF*pf7mbzzQ40UP|8d=O zrDU;rCg)^67VX-P-JUk4zY;e`D(%)s^BSaBQ_lM%pHtN2u8mTz90f zA2fI(mNH;MxTxC{J$lkzd{(SXz@0kh6O7vtAGXm2A-wBKIt`=S2N5n^D<&XaxM0tX z_@X{AzsjIG97x#7Oth`hM#u?uz3vkUUJuWkfzr7s{5B(@f1Aokw>W_#wwlVmpEp`jmEX#o>Wp#t zDi4KBd#kqLmQ`IHXWGS)6JDz$Q(u$aI#|2u$;^G<(FZ9;5&YAcl&?BufCkm|z46P- z625DiuC2txIT^YjUjaP239>`~73^BMp-f)J50y&OS(<0N>bh&6ma_d|4jBI2Nf=LE zerv+O1E&*C*dk`v@R3OpZRpM%vi@+^>KtrVlSmZ0UCdmvZ_mn(y+YtM^G?rrhseYR z3gpYpJ(0}#6K8YtU{sW_iBF5`0`(!OKS2F>`BH($*`&|(Svj*OI8yy|#d8+Gzyqia zr?~UG?(i-yHzisd7|G_Xb@WF)iY~O)nG^SZ`VjX}?UsqlZa*@qJK`wKC)3;dgU4L- zxrQ6)8q8n^4Xnv>K&GLa`aNOL&~=;+ppLnsep|V>p)9Bx?uGUc8Xx{ziS;Q6pWGaY z@W!(VUDHVCUd24#bH_bveJgr+H|M-gPXx11H|2@<8B)a#CI$JZ6)^j^y!=8Ub7jpo z{|*)^g5BHGO(Ra-fo6^?Vb{f)-kH^`V?<@f(KL|OSIRlRCEN5H==|e@fvn-`jcc>DhYezKuyaV0^}qGm*)4Qfep2v{*8ls_h@`tv!+Z=>RUH; zXIYC%*byKH+?3NC>Y|w1o_Q52RlwHaUG|!+y70C3xmt{Y+PY3c% zcbE=#Tw2+q_8R1`s5Enz|HN8Vh&hYaZ7np{_eQ;ll$9X1^B`n9(0b-Yfj64%seAVr*SB_dy!Rl&4rDt8{y|TpKZaufkf~<$bxs zw1@a?l*k@z{n!8-cpT-+l(Dlf0%eo#+s>c)U=S1p%7}q|X5%FE#Tm8iEsRW0X_smf{Gc&REPaqqE=J_Gd zD|>I>#e5g6KY{-Wge*)omUvIrKOQ~4J?{_VOY0k_Nmq1Q!Swld&k+)KE>| z0E}fN1tUsg&mik_V?ysB1!6Djti>g;rPQ*~_=>oY?5wolA$Q@-BYMj??uEBbI$h0J zF)5BP<7xJY4d_D&ucqs^aOQnpkOHD`o3Gpj>iKzT=Ue;Gz;R^LzV?MenD27E*>1C2 zUqeA-9{lYViF6Itzz91xDjReY>pa1y8USjCtI#SlsI#H2MZyr-c+gT)iGR>FBCG>x zZDtMNTd@tuGPx+hL?!e^3ZR# z#$e&qjMwr8E8(r2oYU6;*#*s~roPV~)36EaG|T?jD%uIH;%;ys1yi1+^#2Y7r0ahd z+50Ymd%J$5QjXf#q2k3wNS|nC$yxj?KXAR!SG`PVvm?;{B8BN5$R4o)mW3W=pLFSSP!sD^2aTS}oN_iFiJ(^RIEO^-aGh zT^;zA)v7yNyVpSSq{@w1N$JO_d;Xm`f~g}&5aDwn*v93BDZ#Pb10)I(E(u>Wb9TQ< zGaey~=Oo?_;WBm!>7!hney2(YWqn#$IITMHo+bmG!KW+YH}LpfnRuWMhJf!|Ij7L| z)VqL}7}SVV@!omCOVVDJ2h@*_HtKz>mku8{dPfJDpjHaI2dbErBo|?lvH=)uMQKQxKZ*!8+D6eR~z#PN&mB@#7{^Z_9?-o&R_JiCYW`9{ zmAnuBI2gq_`}1F#D3Ax399mT86Jj#InI2T6x75Nt8lr^WC@##)53$^`} zv_0$G%PM?hvq<&o;>?ezV!v@d!#w6=uQtIfbaY~l#pGV;G`^$$yq|wv)V6M`M;}1}J;K@J z(0eFvYch%cZsI+I&b>#TQGVh%E)(iC@#1`Dxqw%uL8LJXB6@&vMeSA%)EcI(C*bP? zZvR>>#F{CPoS<`FOpL#6L#+gST7MrPFeSGG&*d0}_qsg1VkzzLzXk|SyYj361o-6Z zG+MiFQsv#seosg~p~ioyrdd1Oby;bj=h7r03Yp>6x&ddZu?%BkYFR2uwvlvwZD9rM z9=2x;ST0pSGlhF)kOQJCmB@uuVu{^vLYczz$50VZgh1Ph*p(q&jfVBfsgkyBG!Byuzek^fLu5EVTlzdDa(LjY{)QbqYH>%|oxVNhd;JJ`*WAN(oPqg1!(5^;zDcLuf}>7Kkm={@UgtB0+?)qj+#0THBK32X;>leaH4hd{9Q%)>$dbS1Kn)VG8Txltt@6{LBU<9 zu(g2>l?d7uv7&VYE4#f(BOEH^&9m9_vMS;Ll@S}b6@k8YLOdr79? z1<%b68QmPjc)K*OuR>VcrX(y36UO9e&iJc@8$S6b3UGqgyRmH=*wurIOH5bpMzJ)< zv77q~-eX42mmu+BS*Rt_3&t!z1&SPFo>%PG)PbN@;Q1=OD9y0Btlb2uVhd-#1&vor zM%A<$agL@OTRY!k33`ZgWF0lM8c_P|Cki}Cbr@kD=DVuJ*w6@n&Z5M^`meq#X<(~^ zvPa9VnKZi*GPK-HbQbPZSyUQ{rd)M;P=b!)!WWN8xjW zrw3Kv#AtU9DgOE%w`x%JQ6?VUb;q`Obe}Skz_|Q;YRbh9(`qZ!x?`?91(J>$x~9y` zUlRz~mFCH1#%QMT8Ub5hFa#)=tZG&aNv7cE6)H!^R`BId&bLr%n!B~m*)tj^;HhxG zMJeb&IxrC2n^BhlewWOKC=&T)B~k1o5UG=pyI+`DmtX(lsZCmH^wc>1uC|@n?stUs z(inBcw`<^cIs6leB1$na++f`KzGXK?uDJYQIDSS(B)P?PxzkRI0dH`RdZBH{yFaHS zz~wNyW|EQ{Ht;|da1+;69VeRBXXBAH1A zdmvZT9I)Cz`p4|32c85jhs0z+Msj+wzP7bUuZAd}teB%r>rwF=jQCgK?+5>cXNw9s z{{vxgJy_mkVIYbvjn*3*TYSaL^pxkQeDoOeT;AR9=fGkO+>R}PB&9B!o-z8$<#%~1 zs{vxdy1V8p5V^;$*_9M9&rCAwjeVIHVcXpyU~)R8qB?2EL*K+O+T72WtN#khp;594|8BXsCI+NfYF`U9%Pm z7oM~rA;KUUBE9zSAn|{_^anEi|869_dr5KI7u}=q9NZr8e@4Tup#(3l#ms+%b+1Sn zAz+;U?JklCuA_a~gmet`h(8Yo(zA->cm$&Hqd4;a(HD)8Kz~C=3Kze;KZ+*xtfi?G z#_Ijlf4_D_I_OzfQz}^e%xad0@1X0}I%O*T0)M|9MMA!g-mWn?U$&YxouR#BbrvJ}ZK>5_qwTZ^C zQOk;>H>^Q5z;6of=->Yfwo3m~)=0la<9{SmfW3zN-!O9e-|rVAy6^am2yxVIwNRXt zv$^NGdTi}%i|Zyav82mItW+yFu~7nKpm+af=W~IcNLq62JiMQ4nm;r&bn0^m?dlF; zv@H96f!krv&-sq7L1GU=gP>9BV!>w_uo6W+~I(R7Owwj&l ze+aL3nirkC-UKE#>7XCmKb#Eebt>IIR?C;kUUBdpRllgA2S zM{EjlA@V6j#(a0Ts_O8`)pDG!*zcRphi?w_1^!qOP!;$)* zA`N@kH!t70>@&GHS?+uFZyF%BuK0Qn14MY0%hr(&_G!MfG*}ez31Q<*0HFlbRI5v9 zcd$mQp^x!bQ>@lB`#2YJP7bj+|9(qHZhu`5tY23fSw8cD`pKr^&G~9{2i_YehhKv4 zA}(>6a-3bh=O;22xcgx=^!w%k!SS9OO149>D=R%LP`D<-B5_RKHEjTCDT^`TG+ldl z4TANUm&RNNUIW#0a2h8W-h{;aZ#Az{(hR#idenWuG9hpG{*MD%Y$0S0DRYQp&|Z&R zM(lptYhRTd_dGa0;AI+oD=!>2%1zJJTUpX&SJ|gMS%s>55XOQ}k`CN^hQV;^CC+c| zAQ~`4OomO@*!bYYOHQLId1o)-I_;?e5n}5@lpk4?OBM$Y@!p)ApRjCR<}8k|vYa<& z*wDf`pZ%D)wrpP*oIMd)sWF_{+~-l?XJKxajGSl?rcYk?=!P@Y?IZ`x)=Cb(#j_bI zt8EQ9^?X+w@ZT~}=->Vs^)v6h93O`Hqm=~3;Y~Xlq{N}*w;AzV#hs&S2 z;{OQ!L8XhUKfT}H#EXL}{&dyrw5J8V% zTW|16q84Or|E1jiYfG%}rk~Y@qI6c`-hHD-y>UL~ey)3DNwb@q9GijoPdH=-B5h1S zBzyIk!cJ=-W+{xz_p+0}b1l+h^_bIh;ZLVL(xcVK|O^^MJQRzoG`73hq&>wzSc+CBCWQK)y=3#Pw!#j+u zJv=oE=G^8dv)H2}HYU37YMx8j8{Pppw$k=O5$eG)<1txU+934-TSqf(oW#;td9zE3 zCLhj7Ww%QC%ZgL|DF^hw?qIvE4uwi1b=pEU;WS5d7LQi*lI&`2X=yPi1?jnDp}{d~ zcxXoGyHW98!rs)l9@2uA(}YZ!ha;0$5}H_UW>rX{%csY+F#g-JoB6!-E~odmMtu%# z&OYp)H@!=qk?-{;*QxhMU)b2=#`?UMXWsT}W}V5iz`hfALfmGz1a~8gbk?cOm+7r{7)mvWcQLbdx`J`?943CAKHTn(=5TT4;hU z=Sir}Qu(BD9X&XL^rK%wRPt=+pP#w#i~2G>cjnWO0K&c>aV6}6&ds}nI{_0a#*7YN zCV2n)$!P7&>neY^@fyD<`4Hag)g(low#*u=ly;KwS}7Rl8&SB&=yGQs|P}M+fQk6 zej@BWhl-`ME3V6kwAy)zpRAtqb15HCPa;{#vI%cS-u1i@O#oOj5Nx6>8`d;Ym{Pk_}jB~%9riD&>WMdDO3$X9xPmuvL zu3)TBhC#ARX>{RT%^-apX_LZPiMzW6bJpvN4sp^(`lgmfty4pw%57|PDIib0qbTM@ z4Ke#-f%%Zo{!$?~yf!6gspgwYt$=D@of1suuSyBhlzM#R zFQ4oF*e|<+cdQCE?X+@xWZhYjsEXjERALEn{RS6&J$j_V(`4q?_S3(>^+TYD1B%*H zW!}Q;>_%-gwUF5V06T5U)FNJX;`e=Pp|Rb` z*|(#&5&u$<$Z)9tE(((Gsuxj?=~2+&u^ycAFwOEze;6wr|71GB;8G1mRt$BH#~rVw zYYzofi8Ifi_LHvrsvJ2tfI{wlc=V}MF?N|=*VJ06kqR|ePJIe`>rVKMl-!{Cmyqw56hH!U@51s?A6(KS=4qMO#bTgBFb2xp{$zs z9Stna4i_tD-Ib($7aa2BH`i@Cvz|oNBh3~0G0k@=xD6VUy$I>=oR0evyOA--I4qtE zfMml56UvF9DT;y7wBQwsnf++|gOs z`g#p$>v2&_`?TW;@{D%3)p?nt`d6EOwD#1DZ(rHmN~bBP zsTTIBZC6QEO)8(rr?YTRw4960A~+HERb|NYO-(gx+DN~^%}7etUbCJEdT@#vVzT~_ zroN=D%G_=|bR_!vG1gI{+9}AkBejM(p5?L3Inp=2{87Vd)2&h}m_DkOSmQ7K=zYMb zP$ZMn45u|7fA0-TmgpI+2j%Q7Rs)aTE-d6YyQ8(n1#?}w3GX^(^;j)dF``F9=Ke=> zdpfv|EUIbSyP({%?~pI)@@!)ONksJ@68>XFh3ja0)+8nwHsH>e)IKHzi4N}){oYsvb)z$A35Y7}?s z{BDzuV)g7OC;ac!ng6Bh{a>M7HvdYud3OtG2$Stz77mhH=ORQ^ZV<7Pfuy{8{*QmG z_WgAZhxptSr))>v5Pki8rksTZ(}DpjtGX#6d()h+j{Y6Fay~b2>|E~~92j~0(fq1~ zRPW`!_;_7RnHlxyosjP8d-BUl^PUYL<_KEOuHqFxJmsikDYa`4<< z0~!gc^uZU4asqKZj3IPQnTIy^zZ~M*E<;xF=GRJ0kTq>IVDdvC-&g-x0|C@0CBC(* z0u80=~?1SxSjWJ$Q(f5ad zf98HHEr+gan7l{!MqKUXxWM^)78jVA<3;Up#)dC}Ef7#DfI&$-`Ddm6xa#1wlZStz zxv%EdyzZ%n6Q_T5v^E@oN!c6(h@u zUZ%qQ)XRNqtT{v#7pnPyCu*I3l7sIfovwaJk}~_5^7>S9E*@8HTq4=7 zcQ{rYIB6qY3KNr-Dl}4c1g@Aj4)h_i4Hm>UV))zI@07UrYD*1sq1TsPWAL!f-kR1+ zGb)n|GCTxx36-CKpDw|&4BjX*zlI`#Fkb~I z6{!3`%2;t@uf^IOzi>NXdpdEmqZiTj25uC%GNq@x;WI9}WVGb+*fHx~jpw(8+Z>gs z9yC4ggKAYsT>7WP1)>7?yb24;Vw4{}b^T~EGdqWQ39d0UDrZcX+qv2SsopPS$83Oo z%= zbm?p3?PCfZ*qS<3{?5Xa>}}Ph;E|#$0sHXS8)GI?@Blo#Egqem{y}+#9$N!Ls;639 zqW9h<i!c^7sDbY_L9`&@bt1t zewsjF=whz6!R#TL4r)yJBc}63!0-t2gW>rVyiHrI`9(%lT>Lyp?B9EGJ~pV70}pYd z2E#Gmyn(S!G(x(FEQ;DB>?9ZP9RK1bK&Bn&$BC+x*}PAtE+vcIaTd7Tk}T|bknfp( zP5lDysv<=gO7exg_g!{~?Q==0Vt3sLaSj@EUV|R17mZnyI&6iUFR=xZk3FGQ)^g7% zUSH!RwJa3VdE(1czMRyI9Nj`APu2d6WLk^i^~cpWwYqYpy^rfxXY(=P2SGKUw_wij z_#m$`1y9^Psf#659q$`2MXJ}jw83jlyF#NpC;*o6;Tc5m?V4kQ2pkSyZ8~e&vL^Dd zi)B9R?Kh9E1}oG6(bNBMXXSf@WCvRn-|JH=z`amVUNfvg$+YVO?Soi>ple{^=V{oc z7xj{9Z#2#%W^TEbR7A?ydw5&@#R8#HlJR}aW7-(#^MuPayEnqQUng`$36g|=!xGO- zm-5grd)m=yy!C4_cLzJfWVo~2nUKSG?%>&Xbde@LV%SG57Fk2{hUACv^#Y~BiQY%E z-#-ly8AYAV(j{Cb3kY5^qi>?cfzdM`80(jE3S?0Z2+S@0vT1da#&^~+KoVjLTk`mn7UkseQ{v2>*CxTY>z>#_Pv6zBudcDSZOd9p2n#q-% z@mXxGf`EFn+=H~#Ek7sc#?|M-N4!~-`9n@|K&CB$H6~>87t#$rVN7Zk1(K%26U7H_#&9Y-G0@WmoKFed z_Uz2(m95m2A%REJ8%v6to9O|oFZNcAQlA3YfntVdXMF2H?mfAaQIqC^ z+MOTx^zOoklF&;B88c6gag`~ukIwcD1PX{ebgh!PWC`Dh-`k8gDkJ9*fna~&@UWq6 zLLv#EiJ>}lE4pU9I)bs+G8D9>2`a7Lh7b!;TB&1R)fBHVv`xhwN-C83Bk46{E) z2Ch4piHoo1qLauoI=UXdSKgyk6mU+g)Ixn5wp5mZ4Fc%n5QZojc1l6><=h`EOTXN; zh~i6m`t}s>4;5e|_br5>U_jC&$wK6fVuO@xrMsoUQsW7&G73)luNy9pb@v~uIM`^h z@j+IZWFRrO!WE9XEZk!_1!+Q`Ba0Hooll32q`ElM%jPFkrzN)7Oq}MyEv?!55&-w&#Gpr3jJ?ZiruWQPYgISuUdyOB# zxWZguRZ0OUV65!Bz6b~N+Pr=G5bk{)ag%sh!jbKu;;|dK)S{!@ms$?JxYO!A3bHNn zpcUkVq*R|8e1F5G_R0a_1hvALgoD2Z(fBZ2V0x`fKJkikr&&)ORAKA@nt8XOpYbN% zP6UnpsDet`k$-Lsggt*@2ZZTyX{>V0v4z>*(PoYIw!yvTl0ytW7mq?09S?=mx)k{n zGfASpSaz6kwkvxZ2TyM~4C%l4O|)4yZ%NF}Gz%@qew|z;FRsJ0Q=&*t&nZ8r&HOGT z?ybE?i8{J}d)XD2X?m5Cn&!HbO-;cqTmMFYYix5YPQ=wTDO7!|g|B2gi-V72UvZ!7 z_GMt(6w>n>2XLbx_sY~aH@~z&7|s!pdU-x9P`aid@WY(Pv`tcV{+*gG|B08rFO3fm z>?~WdchPu0!}ceyltEIJ$`|WJ8C{_<&D@6tEjfgbcMtG%)OYDG8xU6w=jk45l9tV* zZ3Rf&#|IedG5L+cnVsd9q4MRI4M=1mUy%6uoo$Mua;1@97~2Wr=5V#=E=a;2B@cbo zXh173{URJMxX%~Fz!@+)NUe2D2n=6j%3H~$eW&RG0W5YEIz0^shOjA z`_-j?T`YNT_hfH@kBx&v3xhNO+%B4>kJ=p;SrXjMQNu}_MimEH8*Zv z9JoCImbJ?c^;1fu%1dZE5u`7h_ycuwY5?>*nak3{*6%D_suh%QIBwqcB~pAfOo^3$-Yx$y_<7DB+JlZv`-x-z%1^$?p)5~vN8loU??`}RYk+Y_q(?I2PykbKgn=W6MDN7$myVf+^$#A-CFhDo89{T3?bn4!GQau&x~ z^W5h7d1>3Kw7x{(MGK33)edn2K9k-txMSjdNk zG*odMAWjEHqBUFfM;){3KCBIx49}~ihxOk`mEg5s6Ej_}G~l1oIQ?M(%Aj7L%ghmeIE5Z>FKoU!OS<6oW(LYy8HunRRvE zYQ`0scwx~6dOO0^Y`&?mcvk-uR+#LhuuS*dl6`d@&RoimZgWdz3+b}5nr}?EsW$#z zbNDD^J>s_5(Y2h(wvi$+?wvr{x>a zWWf(5)C%C%(ec4gt+>ZP1j+W7S~x`Nu1OatCD#VlfaQJ!#9vtOw^~!2aVt5FMmFzu2 z!@hrKVAYtRYvhb?+ro#DgDe^M-hI${Y)w6Cib{c1ohq_oq9CHvQ=~|*e6lIcU2BrY z-_PWc%qfkN?VC9g@e$h~p;HW(OeqjH-ZCK`P&s=37d{bsyEqb;kn#`et*h zn@2x*)dj4s?_FHR&XFHa(B8XYiNMG>6(-y*>A(&7eq;!?SQ>N|SyO_L_`l9Mt_Ba6 zx^H80mXIkU#oS|Dj^0pF!(-D4AysTuinrOjKx*<`J663yF_xVClr^MRC}L@a_2dUY zwi8_vY6$#lIhc;y1<3<~&dTmi%bvnM$om6~HIMX7&vwLpi|>>ZJ6qAJGcF8IaZGjA z=yaqnI3_-#8H?Xsiq}g+UD9}h&G5YJUbWRZ(ZH3rljGr))?WAd!~nl$QT*2uWulIO z(oUr6@G4%NnA3X6PIYtvIZ~rWnp$_Bf{Y0IQdznCHkF_nYXZJ0xgz+j1YCXivE#K_ zpRxIPCd1}0yQbkc2=#8cPQcUQ#z19pl``ja%2$xKa9)74YR$U4u4~2UhzY+S&$u~% zPlre?e-n@qcH=F);pz}{evhPV$1GZD-WwMOklV(IL46o>YBBz8gZaXUvr|d8r#%TC)!$u?f%DY94JP9PCWk<*inPaI)cQVmjxwrjW(W#ZM23!5>x4=^ zwO?qRh`PW8xk6v*Bo+4!=5fLi;)Qb0djiLNgO)2dpgTM^3-0}(?cCd%1n_0BCX0u% zS%<^>rRTMtKEX_M+X=Sk&_E?#Qi1gzJK}z|&jY{O^|ulyhg~;UX`qCLoelVxaEzkzjY)88#OoN+%<&j^F&PJwB$Q3 zLL;8Pz7CL&YpQ%f0k|CA4UK_TU|^Kt0c)wzJ^e&(h_`R^8XWhrv+Az%qofCQTyn*m z(pfVeFFLekWV;&hFG4(ykzFDdctqc8QAXJq0&G$~;2XRs%2HR@uxy5GFnrHzXYBb0PCd7Z)4}@btZvo zlu!Bqzw~5|=$7I*xuHl&*hp1Sn*2p)wJ`ASVNAmvIMzGM-&LSbb&YJ69_BZ`K@Ss~ zt!UA)pdi6d7k~@PZwDpvo!XosIPr0QtYda{+gX1wUEw>5w1GKbPu;e=6}KlSjNf?aE;z?7k%`>!t=;6}6s+EfcH?w=!zXZ}c45|>m| zYaNkHT4pN0WqiPzbZVW;F_uIxwED{iG=Ux2KtLk}nzI~J8?F2qqB1X!<&Rh^v@K)n zRZlhK^D2^YN1AIxY_2^nHdsKVEK+M24!b&au|eifS9AiH+^MuK(=@qu#DFtN5@BL^ zT_sE?dSyIP4~~tNw#!i`L2*9H=&6{B{RL=2(oE>1_}Cmji!3z9Hx_$RM5lfF4}*Af zMe1S)VwO??kc*gIr*qca)TH;{{Fxw6Goyw2*;n|*j-}Y+P7&X#gDN(e@wuHvj5QWQE#VhlDQtPL96AhX>MbrV+g*592$zC!2Z4wujH$$_G`>=7DU*Eu zUTL~hmS>G6*r+~E|FbnbOI?o3HDOBYw%3jC6~}Cb+2?Zk5I{_K5ASIBgP1pUJL_Vi88dRQf2q<7U429r{{p~Vzx_j%TuYN8Tn_% zolgH`eMK~{?AG7d)Vxok{k1^>j!Sk>z0zEXyIU80w$GhnwxZ*7Ou!d8@JZ zds@cxJcgkDrrh9{~te!;f`EBhIOJesqAW@OXUt+3wVi7s}S?6h!r20}SY{DypG zpS9{=R&j8M{*5Dr&p*2gJ5WwkYvvWk?IgBl*K5%M!)Q=12S)!C57dFFi^=2K z5w}?c>j2*a=mp5jPutfEO7f##vRt|Im`}d}Xj3=?#f*-&iL(cWR}kOb3$&ny{i=QE zN43H=2UPVQHBdxs)?5{P`=Zv|_&FAlt-%VHh~T$061if&?@OKmK&=WL$GmEZn2Q2! z?I|fad{fvxGPT2K!-6eL$-|jUL+NCR=29(Fd+7ly&+SL*OspfR@)wB&Le8e+a?Qx+ z(IT1buB!%-e-Fc7rXKq~+3@D@ey(kD#8d&O!}K zO<-xL}%WKVWJ(Jj8`*ZH7&fRBq7M8gci!s{qYut%wf5Ur*^Z3WH!hrp+Z2IP zu)N`fA5bv!M6^JtF;uUDh`7_6_mcyI-FLq{}JiC?cF@=3>V7Z5L#4bh7 zdyx&5B8-*`TiWJ48xHUB`RsC_zVLiYJ=dA-+@KJ1pRQ<^c@h_f+bvw2?2}C&_5ORs zsb++pAT@qQJHxNrry=)-NZP@vSgoo2zIj1gDUZ(!Epd)h>O$*ER1tP5K%4*jQYgue zM0wiBm6LM{GjHVzFSnl%XRiwqJQeqWM4l;+QVp#G_N*~nI>kd-;KIouq-r^}s-k3F0}EwL8ds1pqE>8=!kBmSD@U4d!-YGiiT#H6ldta0Iy0;h>6*C z(`LRSE(kmduVMC;eFn4q#RIODaA|xTh#Le zLb=o4$i`%C#PF}aD(y5WH(lX-AGpJpK8V@FM^C!O2Nq>zNQ9u@p$$|3;4TFU`6$v-Z>(yaRNN~R~5pQ7FPBkTT3kTO+3 z4VB9({nt7gy;WwNna)5;e1GZq`(xzYmTD1}w=W}r5=i?QFq@!ZqL#^)J9qn43PiAW z5IuBeem#EZn~&D#{hc6pfROTbA{RYJoz<8^T5~JYLo_~k|Gc{el%z`x^*Pqoknd77pmFP71oHf;FUa^Roz zbkAi)%9TZC5qmVFDg_>=27Emcblqhq2W~N%YrVx;JBq)HTZ3^=hiKL`SVa23{q}cE z92i~9H@_=(YMZr^w0*47_}fUH=*Pbh0elWD#2bij^+aJTa8nom#viD3YdwoZ6{k2>)`Pv~o~jJkuhZ8Hh1#Xa zqAaSAFIPoy*8x~jGwMR`>6t`rzq7%l<Z^V|7j?*Ff%%NGWW@StJOPT^x=F70fYl3NN5;ioHLg3LACtl_O3_*X{VW5rUD?IZR6C|V>5YliB z!0|QoJN0r4j2CSJ>nlYk&V)~&J{A8|*6Ev)WXgjh`QyT`P6Y>~#@+#kl|;xV0XMXW1dn zZ>+0$9*P?-1@`a-?RZP#$<7=F43t1+6T^CWZzspx+j!z6w5~t{^tK;E^O?_H@vpY7 zlGRYllOfUBwMZ>v`s8;I6~dZB_ghdD$f>E#@giHodp*Lg1`e9TQ(e!6s06fai>fU1*tpF z!U*5QZD{C>R5zSU+{NurYebSV@-(R|O;%tK1Rmby#S3A?YKc%KX$baeo`0XZ3}xYK zalM6vnt`f(!$N+8*3@Zwp4m0?VYF$%)@bYtzv9*;>>?MVt&Rg^DnhF8wf^Lr=9;j` zlaX?wMEg=QeUS@+)nM+meC3EM{%t_pr&$kYu(t{WPs7`sq`1vgk#x_gxjL&oO z{lZtm9dmYQ;FxM^s(4XfzrSPWnP?$~eQbG_$wFz$;0qKfk(IW6+)(v(8%ME(E-t?=I||&1h^N5o>+USML$Trl+Cd?#$z@diOGY6p|t6 z;SMzWo6)*SaA7WoEHvYKj{y`@6I zMlZ0JWt6D|nSU=4Km$6*_A-i(9Zz&W5wwn9XO(#D3#egV_eLduzqB*K{m8`78$eHs zqLz}cv3|dIFsI{>NaM|I!$4Fhq~ZQ4nryh9Tc7rWl}3GzHY{)g5J)>9*!A-s6Zbpn zTRwkeRr(b!)lg0mspH+^WJIzB+ zMN(%a6)3&gso2^qZVZdGDcSmb;>z`6)V+bvjZo~Dh6c{H1A~(`S8&l#(y06w+g^;jbK^uAH!RYZ@S9{68{hodQnL!3T5 zX5>FlCL#v(*8G(v@$~qaQ;cf=63S-i=#F2`;WMbK(g$?e%7vo`0BM-TX%^7eN}N3o z_Ktns0nC&!`=64xQ;>M=5|Q%xM6KZs7&`xF8wUuGz?a?r#kJ|*+NdCqL~ODf1d?z_ zg&XJnpNIHg!Oi_vLzAWrX&wF7FCO{G9u3F;e<^R9Kefz1K7cY|(#3qF(RS8km>Vvgi()% z*edYnzhEf_-Sbw91isJB_0O4}V%7${Vi%ISpwzS9Xlz8d+FIjfkPzge z(btQRPfU*7o^d=R$=ujm^yc1OXY)W6UU0pYY zjr1lJz{T0~&(YD^$AM6(AKP!TbiLmH6f0!IOXVmZlPn~>bR>+SRqczPDuHnz>EwQw zo;PK5Gb)^g=D5|cy`z++RQ6plO$Di z7pl2ordFIoqI=9Lm~tt}!vU={8U+-wiIv}a*7`X?Y0I*9QLjCD3ME!A&GdzY z%6JLU?XQ(TPmzcN-4lPX{&T;`U#jFus5JZfq{m(n)k`!mZ7ahvHl@Uf_nlQ)S81c0 zm5L9CmQgV0L<>AL4sud2 z0X23|+o^wDHzUMAgxJ#iSqGp-VlHRVcCj+K*?{0%v_1IQq?}8C0(8CX;7a^FY{0_N z`Aet@GAH7cGp*v{#-|4n|4{phg~~wMWahGCOhO3<*0)%T8lHXyT*p1RKO64Lo5l*) z28ALleG-l-H{b4WTp@&re+_C@jwa8@tZMsmoIE3Re@T=AdZ$x*i5<4q(bRg+)rw~D zXGLcmU|Vi+fw+2pc=UV)t`27XhAGF+)*fcDLZTULY5y6kXP|L$BWN+B7}~CqQbdn? zr#{67)60hB&k;qV#UU{+u=SVXD0X2id$#|y{_F=!*RsuX{Gb}(ETZ&Yj12tBWk_qr z24V-PI@@no;N^7HVEd&C1xSK-{-ETigP%hL#cia<`71W%^m6VaQvF0P^HDs8mG$ zZFz%Yof|GREYvyvA-k$ut&XrkKm8qH<8YC6wXP-pf~1K{=ZKkE9vp4E|nX&3fXfwu^9gV@3KMp3dpUj!gcl*h?)+txK6ED_c< z_+Egs<;MK$ZoYxEa9OCEFCi}YMWSmyJ^A(`$>^&;sF8A)Og-TugTcd@cR|0T3$WT^ z+3W3YjRp5EgMA&wzlPqJ7~wSRw~&@RZ873-KYZS1lKB-|#RsMRHyH%Hb8B;OKW z&lPBSOId&fKsf$u>;u3M+R^CUDM8ytWXvaKnX%<~@7PExBncXnFJ$}r7_*DcJzPG+ zQ(E^Rx3^?D;oyM=Sz8p^yq{AUpY28h!mChm-*8SjZiJrHcPc;8$ZT4Yt65wZ%#R@L5Fm^EmdYJW~DTpZ(9);LQ8 zMy23?swcq3On@8dLA{rw0gc@Puv$j7KkBq2sm{2 z-sd$lAjB!L-+#Pp*m0Vyw0w)17_ z=c+hpHORW_@XKu{iYDJKAa<)nDuA@&?oP30hd_D&Z`I)(GIaxLK-}9tIW>(OwPnuc zd9W?B8_G5foED}5Y}SU^+He05VYzHSQN1;uSeqa$=AuXUC+d7v1epWb*JfM}2|0U% zxL+$va|A0mH{SwVQNj!@cuvsj{Oye}_J8h;0FW>hnNv{HJIV(nV~5)Bt0A;!MJ-b{ zS)2iyy5jzb{rE_9gpkpEtdCozlv(C4%Z0 zZ+;>gAhE9j){sq)7fa)A1)$Ej)q;i5=ov4 zV@6#b_=VS`vKVPxS%UcG_yNo1N*Pm&SweXib(%!-|lTO`u1wwrtffw*OvoA z-d5`iCIRAgA-s}vQRNUCqq*Gu?rvkX<&EvQbu$hg=e(tHoJZ@2^h&c~l-};?QN!SR z;F}Q(q*`IAL`JtodRniiJlcca!NI|+-N|9)NR!L|jB_}o<i zR*iBNDdr4w$_IX4dL&fS?$l*2Uq8G6!PqXNIIdun5$~XKHIWOLT)E2-?Du(4#$VZ$ zvuVn~GOBgBkpLUs-^~YB;r7d}i---UQLSKZQr0Nu&9Fa!+MSy%cE}?DxkxAU7;i$D z{Obm8QA*bEpUQu-7cpT_2@V%OeY}1{t&uT}JU7eT1ydsD#MMV@BOdhY+v?Y1uNoB6 zP6w!gWeFenMnyCs=$Q$iw-L#OF`3@mXfbO@9A6wxm##n9ZOMup0tNgFf-Ll!aaOYD zmq+gFef&(G0pu&hZWWnpto+*p^ZVU99Qe&+hB7x}@)X#hX@@Sy zuR)X5l)aq&m$ut03oVL}+>YZb5Bm4-d|4H}Oo>gGw)jcN>m}8-0|>@8VngQkOxM|M z*Tas57Y5vCl;Lmccg;DNzsos*C3pvd_V4195gY`Zz$BFZ3|i3hQJPTHN)bpAieuQe&#)^l1T77+4M+LN^x_I*0U%Wo`yjr+ki+Zl2ymWbbD8dPStG&0gD^Wgnn`XLV94QU6gT=xC& z{%1XhDSJ47BmB)!%QWg-j&Q4UZ)a%T^*KT^A6JC*K3KA+2};i-PRQtB8feBXhR1yKBxeKt98)TYrq9d>`;Xf;_U_MOt>>@p6EHQJ(5JA2r-v;= z#~7Q#fa7lu4dO_%E9i8vd?-_eLmI1-@e1xj9Dv(G?Tosdrz*BJcei{qf@w+|<$GR$ zQs`(bXf1%8IeADJiGPCq-c-xAgQB*CiWVA^MZuEpre)?1Z!0g~rUEw6PziMc)iz4G zu^yP1U_bn@$VKoV`5M6w)BXA3r)gea9?+h;X>v?~72+_;MbEmqIONp8(mLhTu$v|u zGj#+IofT_20mYMP4;$uLp%<8K4JEFwkjM4V0cW8~7fIjEd>EJR-No1CF53_3E!^M=oVw;O4`)l3KYtP6d0#nRJbOMzYS#s6WrZ$kt3Q6j&11_3@kKp8 z8G8A?U&T{Te3X&QS6UlSPwk%u1UHLWi@89OOy2VC5{CMK1RNz4`7xw%R+4rSr{#0G zj0#XwJ5U!QqfSGuFuuJ+zv)TG(UZmJzenhO7uDo{2jl%9rk+~vE)ae9bUCfWgccLm z`!-$ceWe7t6>ql-koM^~3D3c|uYcaSd8_q01Y6~wWFf&h)39$+e|43d1*M4$ z6*QGp4n7VyuXbNOiHI7K9C__k9KCc_0HQ1Ly=RgjD6q_az$)cx%mGH;(@c+^+d0w# zg;c`A*PRYm3`)YY=gfNRCgoSAsQt}*1=&!}2W#^UI~7Z7MPODhGSNNG?00$uN{R^U zmWL~o0*e`HJtz!8`Sz~|2{&F zNosEHjeV7@{V~Q*ldn7*J^(Bz%hL09&uluvWx0r~4OV2Gi7VdA3h&)Dygqb4(k08` z*V>Ox%Qhgu9s2ZA2q>)^j|ekbWQFj$BMgnG* zv$M<E&f_3Y_NHT-RP*4m(Gh7XM_G-_?tHjS<}a4;MVnW=9^BRP>L3)XSNVs{PpY zmOshS96{_~T$=u#qzN+-*W)+rRUxqW+-KN#_jJnY)l9z}-+%2QP{H;ULt$fSQx>Ki zBNSJ3<*hUI+a2TU{7pGtZN_`pxZ8$^b3RI}UUx2hkIx`J7hUixo0sq9*N_}g={xT; ze%CDw<6i1}7y8Yu@$v%M0&hS3ovDNp)mG9+2Z z%iHYp!|2X{6Sod^U`ez0l&6_Jvd4yfqM4Q)x(#ZF5MJBE2@?Tf;>H|TcI(ZJ1n85j z2cSH6ZPU={K2;GJuFApswa!Vt9>M<3jav=wex#aT$;~+@pDt&(oLFer=%e!C%~r>v z(P~V?%_2Sf8v|aQ9=g(_et}P&>w(q+VLXkVrXh%#ELhB9p-J` zo@Q&y>du{baW1z+=3AiScH?p}_s$+A)xV7~4MWff(j=s>$2;-$PoSpwx0B@@EpZH-7la(vYcuLBHFj zvtGuC(lA^V45^Q3Fu`kaM)Z6GBs0+N^(;K!B3}0q^;_IjLU3-o$q$gS)cXz*p^AP) zX0eYjC!fvpyYR3CL(y3jrh^-N8Oppd=C1s#odN4K8MFD zBKoU=dN6!`gp~m{{FI~U69cUKr@d(~I>jZd?+ks|Mc0RGaBq^9f?CHwx2mG~C*8VP zniP1$wsS^m+-J%D0o^sVh~c*<{jZ1jPjJL_PV1HIT8eO(d@`4&1gYUz|k2FqQi0jl_LZnCmd20W5kw#in zH$zk?FKco~bbr2e^XCPdrKRhXmpH*}S6RLTH+(JjIG?Xf5Bf_NGwOXfyKp)DfIV&2 z0g1MSKbCc0H}17x)|{__D&@zzwkqFc$pI3{lwQ;3q#Ds*X{HQh+ArRh$|#>pkCTG5 zZppOq|GHcNVfBt#kjaxG#@~ zy8Zhd68)$oiL8a95VEi1jx1SA*_SM3PqM`r#9b6dND*U1mTZ%K9h2KKDNNZJO!j4% z$vVtn&Ufmr`@Wy&cg}fU=XK6`{5h`gwSBJZ^LbyNl}Ss3pdhoEcwzlCD9TUg{DEv9 zbftF`HktJp!!ISwwi@as<&8_8ZCtyJqMuEv{6RL}MjRnDrAMgQLHUI9IpuG`M9SQ7Vq;S|BD4ug4+X#e^ zUVOD!qvxeQ5M#8-cKqg7RV?}Ord+YVd2W9wSZPcc=d;s;1HuxOAU=WNgDUN<-IMu# z0rtHJxRVubej#bWzvf$n;Glxy?Wb@xh1$z({oD-*hH2EA5G|}AaQ8wjanLorvdKQQ z>GF~vDVH|kg(~gxF9>X~d86jx`M_>2OpkABX_-3lx%>;Rr+HThXG5b_i#2&nN>SPN%7ha?F#i?Mp zav;Hm6gGY@H)vO)WcsLY-$v8W%^=#seo(ahgnQr`^k7U3+j>vc{@3n(8hl(L6FeuP zhE1Z?$`{FO$m9c=jMn+RtmDCdNM(KzN$nk;y?E%XP>l*@NppM0Uzk@M12`>>+Y^0%d#I^X^^)!RwF+yu`6HM@W54%fZzzER96 zL@OFU@99{pTJ{iL1-p?NE+kp4IzEp|jqZ*3xFn8!wa~OGK3P=*xDA&OiYkca0c_nO zE`a0GphU09f;tiulF|=p|5RW-bgL*gMHHkuLKGaLWNxH6BINAeLGqB#>c}Cah3C4( z{_1W?jomrCwc^`4Z*fVu#Z6B8uc}iJZMLU4a?6sP8*BMc<*Cesj-{j5`RXbD1+G;ozR#MU(PMgdryW727cO1-{h7e6XP6mU-uZlrk;vrjpS9V z`j}}jaGh}S3UY`f*;9Q{PkUr|Ie!1cH z^W-pr9w#G)XL8AB@9<0p7G)(8L3%-&Mi1ceC6Fb-X~IBj%hmT!Dqp>kD+a%pE;9?s%l+lT5U~!$^9ix-gvYK@CBN8$O4sKP{f*_- zMd(|Cy#>RGht-fWO{Lf=N52-SQO)}esq`D@`L3HA$SZ??-IXa!i1g-bd)PIgt_uzZ#PSqzVMjO*ct(o?QbZ z1=r)FGR?Z6S+Vtq4sF7S&b=c0rTqFy(;LH1o*CSrS;&FmgT*3+@KLKrmC=W75Zy)u zO3@ox{0Cm4iCEDi5%CbXN6M%{==hvv4~|BjXp|=AG|^pjB{FLUZ|7?>cPd>vcew9w z+ho0=iksW?W3|gD!|cAOJ)M3&ZK58P?R4!1hdnuzlS!*HB8d52O{c8%8L*u5uD1+o zrR?k8VL-JoHyvXa7h{_t;e}TxPYg3Bn&(4vvnrXYKk07l9lIQbw^kgiQDM~iD;L&| zs_bh*u4+PSygI02KP_>yMl>-AMHvUubzi{qYWe94=8rt+J2w?Cc&Vm)<%V=O?O&>h zzyQp|?@Es(+*l1xgy_lFi|95_9D|PEWs&mL18ccT<%Nm3^aL9c)+U|nZLqoo?`i=R z&j&Boph(?PA#2cydd-aiQn(SGPZsSMbr83{S5kT28FYdWc5+ZrB~%ok_7t$HgQp)6 z-Le+|?Fq0L#Y><|M84I`NAX6Nl1Z8!^;m{vx|D+bLrR(56YJQURt7ke%o4-1fyKT> z^$u92CBO3AAs%zkjUJ#VxM>qsGLCJ$Hvk?e;8y@Ug`3M#()~6QFy;(<2f(i89!gir zV{kKvF)@d~i9_U}%xP{VtM#XLnD!Mj%XuT|Ro3e-*7C;@7qt~7(lUww9O>`a$dh7*_LEhFKG)rlk zz3|4T2ZdF-|1sk}2W@jDT`AyE(`sj|T2ALheLgPFOGm;(Q*uy0ZdI;)GzU@sR)mj&;;jaX37P3UBzObV`4>>_N93C>gw?8fQxKqxjVLWs z3OH{(!9b}3>Rf#-5RCs6d{D9nx|OmK{30Q6m!CoSmm7a{l$z|0YU)vuBaz3s9>OxO zAHT^n8m|Icd!g4P`!UBYy3)9$wx+F)-TLX_eYC6<)3;*5 z?14v;MD8~EtUYEtx=+#YtEn}R&*@xp^%BSmGk-sJ&CS}^wc)eajwY-{fY)qhF!m|H zcu3Z=$Y9mBfinUIN<94SAo1{rUhA6=0-OIJc-3EtL*KPGpPP)eWNJ_cJis*PcNw(x zW#=!G<(>g1CrD2+_rs&8#1K*?Vg`VoCDZ@l!zad`ET+@|FPh~A8XKv<>t9rK2T^F8 zLLqJH`Kzr?oQO?BK9ue##_s`^!XUQ~;RAJxT>XRLYP`!@*_VJ~VZ##L+NxSkNESt4 z(T+v6dVh4;ajfYUtqb&K2ZjOmybMw(GrPP&_ds=T<|eNf*~can+A?H2FOm~I&Gov> z#@RmNUBcd7nUwXNM`Lp2kwS`7WAzogF|3b`JyGJ7_UCJ&qjT?sKkw<;EDL|JCT}JO zO7)fArklZ@)f4imBiOB~_wq;{5tkg@#~VT(sj4gT%vonirPleNGPh!r*OgncL1~Tf zTH{9vyb^`21ZJ;tR=b!-2d?3x4>mJ9kZrH|;0x{xF`U>^fsg^=867ag6zk`30C5Zb z3}Na4L;;6nP=o$pZuJ`7q}EM{o1^vgs`@XIYsH3M9GF<8WO5h}mKUM%?qiD6 zp)|f0=Gg^`O)IoolR(%|_|`CX?720(A(XoFE~`@_Y-kCOJ*J}~;<9G6BQyBe52m)- z!TeUqM9=>KXQRdhIeop!LdrMw4s<_W;7+hj-e#s{1YTn(N7sXV)ImRHs4SODBkFEi z?7jEf8dlQ&W%pim+YR_sYjPLdc^+s)SkJ7~Czn@+2X$w@$?fD#=-YpK3beU$x29)A zQeeUv5U;71HQmVJLtTAtgJ?vsqkQDbHx2y7gUj4o>*wtOd+3}>Wq293FohXEpN+?) z4k;MalELD_$i6?`O@s-tQQ@~;1SIq#5B<_~WxqFx*9H=;1gMb)6;my57*O%O)KYaTEnS|(}i&@W#`_ORjEE8BsLf}!TdiwKVEy! ze}OOca_nKiZE|*DP!|Y43|c6KnSWP;3EY=v33S!Z8*p+@ntUNK*Aug->XV~-3JULS zr~s_2J{77`eV%Xb=IuCaS(AO7ucY>qwk#Ta{KyG|pIbf-F;pyoM=o|ZU#_8y0X`?G zCVf6~<8owvKmPE$<8L+mfk<@s3 zpC$SD{`;4^tjslm@M(fDO(WBzIuAE0T_ooNMN^tF%TPrDfeEpa*0A5OvyWq606Fmz zPlKL@8#-RRHss>8H?}b53S~F(kFuoBf1>B5O`box6=;S1YD9K>xo)kvvQVyBe%+M8 zABl!GNI(gAO0@)!Ab;PWZd@1jrZYBV8nH9(8%w*CAC@UC1O&*ojXiOAeX;@qrRYqD7%%)f?qt+HMLvi0Olo;-;SUC$UYAlr)X zFviNTjq>3H|tB=B~cSs z+Cow+vUzI1cf+JLjY!(>S<5xn2n>m;tx1hq*UMW^^Y8?6>qz%TUg*JfvgSfk19!jbx8HKJmpZ|i?T3y1H?unw zITm`fD`e!g>hD<_nH!mXZJ0UCJui51 z30@EhIlmg*ufg^R6|X4+Z)Tf+knn3bT@= z;UfHnNg!6~X& z%eqm&RM?F^jC~1I2&C_e6*^-^ns%vn8m4?*LgI=;*cZBu5FpljAUE81mj-QZ?Fztb zIjxvGxiqIhPXy?CPzwlXPXZE{-0zQi8BtX45FUV;D!J|Q<)}_m(@#sTHt0q+Z7X_b zOK69lqF(J7)MDNV2!cCRD^=YB(l4bn`gQ`j13!(H-B=1FY$D#NTD1h`jvUp|`R)Vv zBbK4TuTOdy1BGUe4HGE0HrMorpBhqmch?P@xM@&yAf_pp@=K+`E z8I59*dEa0vjC%#!i53vf|5XFyd+ou$mp?w!fMVvR=WPHo>%F9# zEx%qlzm+A&-TM%@Lm(cJP{&yBa-;FJ!YC zKQgpnj|c{DXfC1-Pq1q|4jPqr?*q)PR6ez^-W3I=wHoNHO&S`|sgxHtu`5i_-h3pL zUV!5-+$;@v)8`+0P2!5i%?xodVUO#*RVv+ix2DWaANxa!>0Q0(G1ff2036eA>9Cp-uMW(AK0!P%Y-_i&j zYJ7RKMN)8hs!28on}r&v>+A$n-$_R-zk5-CJfsEXT@}7;5WXkji$y-1wf=1dGXv@u zyzob?GKYV|5c|`w*I2bxzUnAHIh0yU=8Y6KX@>@|yixZ3J#)5pLbGutg*!}jZv_LE zrIKat9Bhv^*2&m9h$J{L6gMUsXE-dR1wKb75?B3Tr*p1Rw~>2<*#XJyXcqQ7HqE_s zPh8Ck(t_OwLEQy+!`P&oT|F9h36??LA|vVtPXi}DJ08?uFk}aTzRJqd6amU>Ws`7d z^rjFE?j#;BbG-8ARO;KU6{q+Fmr<R8zT}S^zHoR2<5#)OEqZto@v05*#;VXwGb!>egREt|`$8(eHz-Xp}NR#4h5lvidCnEe3*Rza4 zuSlB0Q7d;f-c(~R6;U-CNMhKFPp1(N?i)9(6&0)9QA_f!`88ClI`xjGbG~s`Kqsb~ zJF-DU*Z$j~h^ncFu)S5cT@BbLmj*1yJj|;q@vYoo?~|CcLpHzZq*@uy4+yCBTmWs} z7xK%B)d|VJPF5LBfR2;b6MP*~f>uINuikRCxy91P%Ni)JU@iuVrVMmXWN{JDrc+5D zCtbYj^L}TiCjdGU%*Ux$Tk;T~=Oh<1FHkD6UN^qE%J!JgI02pIhfRTPR9?Ue@E1uqj0@j8m-->jOO`Qk-X4|x_Q$0p08V&?`q|$Z1MS%t39~C zZ;-oA+#%`mLjfKUiq6_yb5COZ#*g5OYGy$va9c!pA>*gvhSjCiH-|JY7~DBZY!4K=+J8H3&{InH9RfY)h&=flL#!zGFEUF- zF&;Cx2>W>{Y?uz7PPXI!d8n!mv1*CdEC1{;R&<2t=f8B4z2kp=C2XLm`}5o+iSp(C z97{hr;o47LC)(K|{#}yZW;&hh>hg2@ju}+Q{-gWs39E|x5tNm)$TxV(@|~j}wl$|A zfLk)p%|E}Jn`cMjtq=3?$FJ@IK|k@rzyj!fY_=UvyqxD)nvK*sLR5Mlpx%&za$9szVHm_JDG2op1*N;fQsBPb@(Hu~$xvxjrZD{rI#yv4RZk0QD>XvBY#u9zZqq5v`;*eoFSmcSp8?${rXRFt9KXnVff)%p@t<01 z;6PPUQ16vS56l$YwGJI3K|nSh6>O-+cXJ5v{lDu zFsfM%t{!r!G~C(PZ5ajIC;HmsvJJXu}nMC8)>M^9Ta4wxMQ$cqusw>7QWg1<7L zl{Z1~9zx5jT<@&uxBB~7Dxeh{8k~T{f1Zq!zFWp<_Ivirh^>z*30i(DL%}4I0u%W0 zNd0}?^PmB9R<39zmTXN&2HU`rPXaUl^l|M3kkdjF0-G2b} zB(>=%tk1npO+0~-e1V{JJpx!}nIuu-JSfRKiqT9X&rJ3KH&3LA^jMRNi<6@TopV^p zFq#SVa7X^(&=H@*c&ziB_>XZdL99Q{a3MS7i5k8+#5$C=NAAlgjfv~szjhNOJi_$- zqB?)OC<6*Pco-mSA%M4r4M7d_c#i=G-UOxbOAuJt3b&GP`uHF*#Vf09a5rSDe!duu z(RdMMV&^sQ)!4hrS>Ahx0In)1D0b->OyI^~id_jx>KQOW4U6&MG28@-b8*S5jKDJ_ z*c|??m1|)8bcE&0h`l0D`y6G>SOVtqfS(;TM1RfuPr^062lamv{!HJ~_fZ^?lzYvf z6LHeC+CVHdoL)j-Q?p#97#AFmkp1}cqNwP#-_F*15{ z1gojU=Pq2RWAwz|u4<-uAi8t1m6R`0))2Zi=440ahHTyVGHZ)(t{=5v)#7-*Pe4z+ zCHmr<_XesRMrt&StIPanJ|xL?U|#7q2B91huw)dJT0DRfPz#bA--nZWVBHMjg3qTI z+{w~gH>NF1!*f{e{lfTx4agZ;Jtgv>y+)|)!L2QI(GW#|TYta9?w}&tsck%^!`5R{ zt)OTe%95S#vREXy1z==Q6Pm@rn2C8-}roL=(wlHIQO$1d1 zdE0a4H$)6b;&Jf$;#Lxop*b!Pol+J-yAi*;$?AN6 z9;Z1_zy;%G1$a=`{9x@U45}d&vEG`VQyTF)zq7N?x=P>t;q0N_h$efu zl(A6f{40~idJ<;|3P zNOLE(@{q(hGPM10CM`3wM9{`r*I2Vg4b&Z6N(7ylN4$DfR5rAy(u%PtH344eg~B)E z6Ma@sO;^?+l$s_xGHl3URii@4%=QvQeY3>RZT=;UDKaeC{H~#aWgvOHwC`-;akk6z zKC&74(DHdIkruAdI41^%`UM#c3q;N}T9Eg{R(3w%uh@fFT{spVq)O%hxmZ}I5JL$_ zGAmE#!yi109|i!S+{eK3qK|6r^#JfIu;hW11QN}OWE@z zx3xC#(825=k?patPis|f*a?{B7;rba=uDOMsNTL_V z_?|A|w_M0c<6zKI@R96TLyq)wV(d52%!3-uIN)KlRaA_8EBJ!4r)hBi;&(#*weEYL z8N@Glg#m=wi*t4gUy&Hxe-ij$G~&J{P9RcvVR>Bb+Ah~SD>ZQkIWHlhj=Do;wl&@w zBE0RFhgQ6f4jjz(b$~!^(32yuRj1uH=;EeAvR&bK9@{brz#|z?fEg+;fKGHs*ss1j z_WCx!n@7Jb`+EQktVrfK^46^5h^G>^=U;P_)G~mO9TK-!?;ZF0)`o4yR0|Vz+*R+- zd^j~NLZLrp2DX)nkA!HV8sy!MrQte!tR zl=4Z#5)W-GxD@xQm9k{jIVcZMB>m7RfSe>0X+I!)^BxyMDi-WA_yyqF*ZED8*{*$; zz5iw4_x~TKg+n?e+O1kzc=2+7$5`0pRBBvHRJ1~~+mFNc9c{aZ?|-Af+ud7Qc<|Cd zb8+DxkgT6iR=b^W3utXB7eW7Bd_!^}SOO?B~u+6n3P-NJHR#aKww|?b!$e+Iqwu zX=?x4sx+8lxhTPHZ#Slw9QPXZClx6hQKk{+j7?1PImJxgU<4flJB$!%r-18_Xi0Uk zi}%H&j|Eo?+MTmy7ZakkiN#U%TIy=+Fm*!!Ahi3N%77zS(RboHYH9QH$Cx;migIvA z`?hkKe7+^{3S2h+`J~&NRB8?}1_0J#mH)igdNYiR>N2*m5zw$j@#Vs76aiF6sa$0c zCjudnARaEJY)w0Amz6^Cd`~0sB9Wv>@ARTCuFemLZ(Fva^jcnA;=!S$SyGRyFt^w7 z2z{At3=T(be!c(B=B0R9J-}lFR?iw@>vVbKlEb%ixdyb2TiF>@lnCfU=kCUNbFP=qC9jnTO0f{l|7{l`_`2RT`Ky;BN0QHln%Grit=mw$=g%)G?tfan%5-4F$D}@zXEH zLv1n3@Lg-1i>2b!HFbR`ah=v*u5m})et%O!ttM$^M46RAyaR|aCXR(rz8AcHU#|yR z`f|N19osA0dPpm8;rIk?Pv}Ou*nT`36Q8vmLLrJj2Od3nH|3w+;*7vDEVRTdaW@@Z zcY{lSP~)M6m-=F6-U;LZVM=7;Mz5iC^*$QvGWyO*n^#E4)WyX`)F63AYESfA%BD0`Nr2Xax-u8e- z2j)!UG%Yi*bO;Pom@b6y8)TK{9*>;buG7`Yu6tQ0oh)jwW(R2SCY>)bjyFSJGZdK)2(!cw zz2VDl(z`L9uV(;#o(IP)e^Qy<}`2n-8 z)1K@Olg3KYhWJJ*UbO}TPDJ9(x}QC5&&Od>W-be~1FS;S02amgnK9m~GMJ=O4KogJ z_nd7_yb;kszjMF@FmsPmU5wNps5g5C2HL5+w{2YqQ5NH*EvBbaD`E|&rwM(Iv~G;s z0kgLJGy_gH1q`$e!Es->tPNXTi;(!^(>kA$1;>S`?msVPv1PgN;dayJc{6{4Pw<;* zOrqsHpeTIy)C+^^P{3(lOLOCHSDE6f+_p1CLeK4WB4}3v@qV<`V_ZW|p*z&L^&YaS zfWa1>&50Vk;EFuyu9J+rOEcl}5e{Wqb(8cC!=lJ=S-HY-G{6>mC6M059ePYVv(oJO zUdTqL9?SR6%%)u>Ps*Lto2po{fmd`{*(naOElh|b#K*E>J2_8azOw^`iO;ONt2zI0 zFxoU&l69J&=V!xU742zw7CuRLs#bg80v>2 q?#DPkbcH`=^#eN4M_3!*z~75ZSomtDM+KN0$l!{JPTA#KQU3#G`L#U& literal 0 HcmV?d00001 diff --git a/docs/docs/usage/oauth.md b/docs/docs/usage/oauth.md index c8dff35908..30cd842bff 100644 --- a/docs/docs/usage/oauth.md +++ b/docs/docs/usage/oauth.md @@ -28,9 +28,17 @@ Before enabling OAuth in Immich, a new client application needs to be configured 2. Configure Redirect URIs/Origins - 1. The **Sign-in redirect URIs** should include: + The **Sign-in redirect URIs** should include: - - All URLs that will be used to access the login page of the Immich web client (eg. `http://localhost:2283/auth/login`, `http://192.168.0.200:2283/auth/login`, `https://immich.example.com/auth/login`) + * All URLs that will be used to access the login page of the Immich web client (eg. `http://localhost:2283/auth/login`, `http://192.168.0.200:2283/auth/login`, `https://immich.example.com/auth/login`) + * Mobile app redirect URL `app.immich:/` + +:::caution +You **MUST** include `app.immich:/` as the redirect URI for iOS and Android mobile app to work properly. + +**Authentik example** + +::: ## Enable OAuth diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index 54234e4b08..5aae5cfa69 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -33,7 +33,7 @@ if (keystorePropertiesFile.exists()) { android { - compileSdkVersion flutter.compileSdkVersion + compileSdkVersion 33 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index 36a967da6c..7363e4999f 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -12,15 +12,26 @@ + + + + + + + + + + - + android:name="androidx.startup.InitializationProvider" + android:authorities="${applicationId}.androidx-startup" + tools:node="remove"> diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 826f5d017f..fd3dadcadd 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -109,7 +109,9 @@ "login_form_err_invalid_email": "Invalid Email", "login_form_err_leading_whitespace": "Leading whitespace", "login_form_err_trailing_whitespace": "Trailing whitespace", - "login_form_failed_login": "Error logging you in, check server url, email and password", + "login_form_failed_login": "Error logging you in, check server URL, email and password", + "login_form_failed_get_oauth_server_config": "Error logging using OAuth, check server URL", + "login_form_failed_get_oauth_server_disable": "OAuth feature is not available on this server", "login_form_label_email": "Email", "login_form_label_password": "Password", "login_form_password_hint": "password", diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index 7310ec8756..ffa1e57887 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -3,6 +3,8 @@ PODS: - flutter_udid (0.0.1): - Flutter - SAMKeychain + - flutter_web_auth (0.5.0): + - Flutter - fluttertoast (0.0.2): - Flutter - Toast @@ -37,6 +39,7 @@ PODS: DEPENDENCIES: - Flutter (from `Flutter`) - flutter_udid (from `.symlinks/plugins/flutter_udid/ios`) + - flutter_web_auth (from `.symlinks/plugins/flutter_web_auth/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) @@ -60,6 +63,8 @@ EXTERNAL SOURCES: :path: Flutter flutter_udid: :path: ".symlinks/plugins/flutter_udid/ios" + flutter_web_auth: + :path: ".symlinks/plugins/flutter_web_auth/ios" fluttertoast: :path: ".symlinks/plugins/fluttertoast/ios" image_picker_ios: @@ -86,6 +91,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_udid: 0848809dbed4c055175747ae6a45a8b4f6771e1c + flutter_web_auth: c25208760459cec375a3c39f6a8759165ca0fa4d fluttertoast: 16fbe6039d06a763f3533670197d01fc73459037 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a image_picker_ios: b786a5dcf033a8336a657191401bfdf12017dabb diff --git a/mobile/lib/modules/login/models/hive_saved_login_info.model.dart b/mobile/lib/modules/login/models/hive_saved_login_info.model.dart index 6d367d5978..e807fc4780 100644 --- a/mobile/lib/modules/login/models/hive_saved_login_info.model.dart +++ b/mobile/lib/modules/login/models/hive_saved_login_info.model.dart @@ -5,21 +5,25 @@ part 'hive_saved_login_info.model.g.dart'; @HiveType(typeId: 0) class HiveSavedLoginInfo { @HiveField(0) - String email; + String email; // DEPRECATED @HiveField(1) - String password; + String password; // DEPRECATED @HiveField(2) String serverUrl; - @HiveField(3) + @HiveField(3, defaultValue: false) bool isSaveLogin; + @HiveField(4, defaultValue: "") + String accessToken; + HiveSavedLoginInfo({ required this.email, required this.password, required this.serverUrl, required this.isSaveLogin, + required this.accessToken, }); } diff --git a/mobile/lib/modules/login/models/hive_saved_login_info.model.g.dart b/mobile/lib/modules/login/models/hive_saved_login_info.model.g.dart index 80e6f30a9d626b8bc69d16e075a5b5ca43358b77..27c1d19672a7a6555ae8945b7533ba4713dc1bb9 100644 GIT binary patch delta 151 zcmX@l^?-W=D|3pitwLUDPL6`TLRw-@ajJrqLRw~OPD*jKajZgOu|iUQevS^80vIGF zC#Mz{hva9c=D}5~?6P*+E22TB072bUCO=A}>OVJ>Dgoh-;AGIhHiFY;bsH?4=XO5 delta 35 rcmaFBeV%IrD>Hjyu|iUQe$M1f=HkiQSOg}&VctHufLV0%8 { return false; } - // Store device id to local storage - var deviceInfo = await _deviceInfoService.getDeviceInfo(); - Hive.box(userInfoBox).put(deviceIdKey, deviceInfo["deviceId"]); - - state = state.copyWith( - deviceId: deviceInfo["deviceId"], - deviceType: deviceInfo["deviceType"], - ); - // Make sign-in request try { var loginResponse = await _apiService.authenticationApi.login( @@ -97,65 +88,15 @@ class AuthenticationNotifier extends StateNotifier { return false; } - Hive.box(userInfoBox).put(accessTokenKey, loginResponse.accessToken); - - state = state.copyWith( - isAuthenticated: true, - userId: loginResponse.userId, - userEmail: loginResponse.userEmail, - firstName: loginResponse.firstName, - lastName: loginResponse.lastName, - profileImagePath: loginResponse.profileImagePath, - isAdmin: loginResponse.isAdmin, - shouldChangePassword: loginResponse.shouldChangePassword, + return setSuccessLoginInfo( + accessToken: loginResponse.accessToken, + isSavedLoginInfo: isSavedLoginInfo, ); - - // Login Success - Set Access Token to API Client - _apiService.setAccessToken(loginResponse.accessToken); - - if (isSavedLoginInfo) { - // Save login info to local storage - Hive.box(hiveLoginInfoBox).put( - savedLoginInfoKey, - HiveSavedLoginInfo( - email: email, - password: password, - isSaveLogin: true, - serverUrl: Hive.box(userInfoBox).get(serverEndpointKey), - ), - ); - } else { - Hive.box(hiveLoginInfoBox) - .delete(savedLoginInfoKey); - } } catch (e) { HapticFeedback.vibrate(); debugPrint("Error logging in $e"); return false; } - - // Register device info - try { - DeviceInfoResponseDto? deviceInfo = - await _apiService.deviceInfoApi.createDeviceInfo( - CreateDeviceInfoDto( - deviceId: state.deviceId, - deviceType: state.deviceType, - ), - ); - - if (deviceInfo == null) { - debugPrint('Device Info Response is null'); - return false; - } - - state = state.copyWith(deviceInfo: deviceInfo); - } catch (e) { - debugPrint("ERROR Register Device Info: $e"); - return false; - } - - return true; } Future logout() async { @@ -215,6 +156,74 @@ class AuthenticationNotifier extends StateNotifier { return false; } } + + Future setSuccessLoginInfo({ + required String accessToken, + required bool isSavedLoginInfo, + }) async { + Hive.box(userInfoBox).put(accessTokenKey, accessToken); + + _apiService.setAccessToken(accessToken); + var userResponseDto = await _apiService.userApi.getMyUserInfo(); + + if (userResponseDto != null) { + var deviceInfo = await _deviceInfoService.getDeviceInfo(); + Hive.box(userInfoBox).put(deviceIdKey, deviceInfo["deviceId"]); + + state = state.copyWith( + isAuthenticated: true, + userId: userResponseDto.id, + userEmail: userResponseDto.email, + firstName: userResponseDto.firstName, + lastName: userResponseDto.lastName, + profileImagePath: userResponseDto.profileImagePath, + isAdmin: userResponseDto.isAdmin, + shouldChangePassword: userResponseDto.shouldChangePassword, + deviceId: deviceInfo["deviceId"], + deviceType: deviceInfo["deviceType"], + ); + + if (isSavedLoginInfo) { + // Save login info to local storage + Hive.box(hiveLoginInfoBox).put( + savedLoginInfoKey, + HiveSavedLoginInfo( + email: "", + password: "", + isSaveLogin: true, + serverUrl: Hive.box(userInfoBox).get(serverEndpointKey), + accessToken: accessToken, + ), + ); + } else { + Hive.box(hiveLoginInfoBox) + .delete(savedLoginInfoKey); + } + } + + // Register device info + try { + DeviceInfoResponseDto? deviceInfo = + await _apiService.deviceInfoApi.createDeviceInfo( + CreateDeviceInfoDto( + deviceId: state.deviceId, + deviceType: state.deviceType, + ), + ); + + if (deviceInfo == null) { + debugPrint('Device Info Response is null'); + return false; + } + + state = state.copyWith(deviceInfo: deviceInfo); + } catch (e) { + debugPrint("ERROR Register Device Info: $e"); + return false; + } + + return true; + } } final authenticationProvider = diff --git a/mobile/lib/modules/login/providers/oauth.provider.dart b/mobile/lib/modules/login/providers/oauth.provider.dart new file mode 100644 index 0000000000..0470d539a5 --- /dev/null +++ b/mobile/lib/modules/login/providers/oauth.provider.dart @@ -0,0 +1,6 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/login/services/oauth.service.dart'; +import 'package:immich_mobile/shared/providers/api.provider.dart'; + +final OAuthServiceProvider = + Provider((ref) => OAuthService(ref.watch(apiServiceProvider))); diff --git a/mobile/lib/modules/login/services/oauth.service.dart b/mobile/lib/modules/login/services/oauth.service.dart new file mode 100644 index 0000000000..995aef2757 --- /dev/null +++ b/mobile/lib/modules/login/services/oauth.service.dart @@ -0,0 +1,39 @@ +import 'package:immich_mobile/shared/services/api.service.dart'; +import 'package:openapi/api.dart'; +import 'package:flutter_web_auth/flutter_web_auth.dart'; + +// Redirect URL = app.immich:// + +class OAuthService { + final ApiService _apiService; + final callbackUrlScheme = 'app.immich'; + + OAuthService(this._apiService); + + Future getOAuthServerConfig( + String serverEndpoint, + ) async { + _apiService.setEndpoint(serverEndpoint); + + return await _apiService.oAuthApi.generateConfig( + OAuthConfigDto(redirectUri: '$callbackUrlScheme:/'), + ); + } + + Future oAuthLogin(String oauthUrl) async { + try { + var result = await FlutterWebAuth.authenticate( + url: oauthUrl, + callbackUrlScheme: callbackUrlScheme, + ); + + return await _apiService.oAuthApi.callback( + OAuthCallbackDto( + url: result, + ), + ); + } catch (e) { + return null; + } + } +} diff --git a/mobile/lib/modules/login/ui/login_form.dart b/mobile/lib/modules/login/ui/login_form.dart index ea741faf1e..82f723f01e 100644 --- a/mobile/lib/modules/login/ui/login_form.dart +++ b/mobile/lib/modules/login/ui/login_form.dart @@ -6,11 +6,14 @@ import 'package:hive/hive.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/hive_box.dart'; import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart'; +import 'package:immich_mobile/modules/login/providers/oauth.provider.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/shared/providers/api.provider.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/shared/ui/immich_toast.dart'; +import 'package:openapi/api.dart'; class LoginForm extends HookConsumerWidget { const LoginForm({Key? key}) : super(key: key); @@ -23,10 +26,47 @@ class LoginForm extends HookConsumerWidget { useTextEditingController.fromValue(TextEditingValue.empty); final serverEndpointController = useTextEditingController(text: 'login_form_endpoint_hint'.tr()); + final apiService = ref.watch(apiServiceProvider); + final serverEndpointFocusNode = useFocusNode(); final isSaveLoginInfo = useState(false); + final isLoading = useState(false); + final isOauthEnable = useState(false); + final oAuthButtonLabel = useState('OAuth'); + + getServeLoginConfig() async { + if (!serverEndpointFocusNode.hasFocus) { + var urlText = serverEndpointController.text.trim(); + + try { + var endpointUrl = Uri.tryParse(urlText); + + if (endpointUrl != null) { + isLoading.value = true; + apiService.setEndpoint(endpointUrl.toString()); + var loginConfig = await apiService.oAuthApi.generateConfig( + OAuthConfigDto(redirectUri: endpointUrl.toString()), + ); + + if (loginConfig != null) { + isOauthEnable.value = loginConfig.enabled; + oAuthButtonLabel.value = loginConfig.buttonText ?? 'OAuth'; + } else { + isOauthEnable.value = false; + } + + isLoading.value = false; + } + } catch (_) { + isLoading.value = false; + isOauthEnable.value = false; + } + } + } useEffect( () { + serverEndpointFocusNode.addListener(getServeLoginConfig); + var loginInfo = Hive.box(hiveLoginInfoBox) .get(savedLoginInfoKey); @@ -37,6 +77,7 @@ class LoginForm extends HookConsumerWidget { isSaveLoginInfo.value = loginInfo.isSaveLogin; } + getServeLoginConfig(); return null; }, [], @@ -67,7 +108,10 @@ class LoginForm extends HookConsumerWidget { ), EmailInput(controller: usernameController), PasswordInput(controller: passwordController), - ServerEndpointInput(controller: serverEndpointController), + ServerEndpointInput( + controller: serverEndpointController, + focusNode: serverEndpointFocusNode, + ), CheckboxListTile( activeColor: Theme.of(context).primaryColor, contentPadding: const EdgeInsets.symmetric(horizontal: 8), @@ -92,12 +136,52 @@ class LoginForm extends HookConsumerWidget { } }, ), - LoginButton( - emailController: usernameController, - passwordController: passwordController, - serverEndpointController: serverEndpointController, - isSavedLoginInfo: isSaveLoginInfo.value, - ), + if (isLoading.value) + const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ), + if (!isLoading.value) + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + LoginButton( + emailController: usernameController, + passwordController: passwordController, + serverEndpointController: serverEndpointController, + isSavedLoginInfo: isSaveLoginInfo.value, + ), + if (isOauthEnable.value) ...[ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + ), + child: Divider( + color: Brightness.dark == Theme.of(context).brightness + ? Colors.white + : Colors.black, + ), + ), + OAuthLoginButton( + serverEndpointController: serverEndpointController, + isSavedLoginInfo: isSaveLoginInfo.value, + buttonLabel: oAuthButtonLabel.value, + isLoading: isLoading, + onLoginSuccess: () { + isLoading.value = false; + ref.watch(backupProvider.notifier).resumeBackup(); + AutoRouter.of(context).replace( + const TabControllerRoute(), + ); + }, + ), + ], + ], + ) ], ), ), @@ -108,9 +192,12 @@ class LoginForm extends HookConsumerWidget { class ServerEndpointInput extends StatelessWidget { final TextEditingController controller; - - const ServerEndpointInput({Key? key, required this.controller}) - : super(key: key); + final FocusNode focusNode; + const ServerEndpointInput({ + Key? key, + required this.controller, + required this.focusNode, + }) : super(key: key); String? _validateInput(String? url) { if (url?.startsWith(RegExp(r'https?://')) == true) { @@ -131,6 +218,7 @@ class ServerEndpointInput extends StatelessWidget { ), validator: _validateInput, autovalidateMode: AutovalidateMode.always, + focusNode: focusNode, ); } } @@ -200,13 +288,9 @@ class LoginButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return ElevatedButton( + return ElevatedButton.icon( style: ElevatedButton.styleFrom( - visualDensity: VisualDensity.standard, - backgroundColor: Theme.of(context).primaryColor, - foregroundColor: Colors.grey[50], - elevation: 2, - padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25), + padding: const EdgeInsets.symmetric(vertical: 12), ), onPressed: () async { // This will remove current cache asset state of previous user login. @@ -238,10 +322,101 @@ class LoginButton extends ConsumerWidget { ); } }, - child: const Text( + icon: const Icon(Icons.login_rounded), + label: const Text( "login_form_button_text", style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), ).tr(), ); } } + +class OAuthLoginButton extends ConsumerWidget { + final TextEditingController serverEndpointController; + final bool isSavedLoginInfo; + final ValueNotifier isLoading; + final VoidCallback onLoginSuccess; + final String buttonLabel; + + const OAuthLoginButton({ + Key? key, + required this.serverEndpointController, + required this.isSavedLoginInfo, + required this.isLoading, + required this.onLoginSuccess, + required this.buttonLabel, + }) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + var oAuthService = ref.watch(OAuthServiceProvider); + + void performOAuthLogin() async { + ref.watch(assetProvider.notifier).clearAllAsset(); + OAuthConfigResponseDto? oAuthServerConfig; + + try { + oAuthServerConfig = await oAuthService + .getOAuthServerConfig(serverEndpointController.text); + + isLoading.value = true; + } catch (e) { + ImmichToast.show( + context: context, + msg: "login_form_failed_get_oauth_server_config".tr(), + toastType: ToastType.error, + ); + isLoading.value = false; + return; + } + + if (oAuthServerConfig != null && oAuthServerConfig.enabled) { + var loginResponseDto = + await oAuthService.oAuthLogin(oAuthServerConfig.url!); + + if (loginResponseDto != null) { + var isSuccess = await ref + .watch(authenticationProvider.notifier) + .setSuccessLoginInfo( + accessToken: loginResponseDto.accessToken, + isSavedLoginInfo: isSavedLoginInfo, + ); + + if (isSuccess) { + isLoading.value = false; + onLoginSuccess(); + } else { + ImmichToast.show( + context: context, + msg: "login_form_failed_login".tr(), + toastType: ToastType.error, + ); + } + } + + isLoading.value = false; + } else { + ImmichToast.show( + context: context, + msg: "login_form_failed_get_oauth_server_disable".tr(), + toastType: ToastType.info, + ); + isLoading.value = false; + return; + } + } + + return ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).primaryColor.withAlpha(230), + padding: const EdgeInsets.symmetric(vertical: 12), + ), + onPressed: performOAuthLogin, + icon: const Icon(Icons.pin_rounded), + label: Text( + buttonLabel, + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), + ), + ); + } +} diff --git a/mobile/lib/shared/services/api.service.dart b/mobile/lib/shared/services/api.service.dart index c1b70a0e81..900e261e3a 100644 --- a/mobile/lib/shared/services/api.service.dart +++ b/mobile/lib/shared/services/api.service.dart @@ -5,6 +5,7 @@ class ApiService { late UserApi userApi; late AuthenticationApi authenticationApi; + late OAuthApi oAuthApi; late AlbumApi albumApi; late AssetApi assetApi; late ServerInfoApi serverInfoApi; @@ -14,6 +15,7 @@ class ApiService { _apiClient = ApiClient(basePath: endpoint); userApi = UserApi(_apiClient); authenticationApi = AuthenticationApi(_apiClient); + oAuthApi = OAuthApi(_apiClient); albumApi = AlbumApi(_apiClient); assetApi = AssetApi(_apiClient); serverInfoApi = ServerInfoApi(_apiClient); diff --git a/mobile/lib/shared/ui/immich_toast.dart b/mobile/lib/shared/ui/immich_toast.dart index 80cac0ce96..1bc0bb4ea8 100644 --- a/mobile/lib/shared/ui/immich_toast.dart +++ b/mobile/lib/shared/ui/immich_toast.dart @@ -9,6 +9,7 @@ class ImmichToast { required String msg, ToastType toastType = ToastType.info, ToastGravity gravity = ToastGravity.TOP, + int durationInSecond = 3, }) { final isDarkTheme = Theme.of(context).brightness == Brightness.dark; final fToast = FToast(); @@ -77,7 +78,7 @@ class ImmichToast { ), ), gravity: gravity, - toastDuration: const Duration(seconds: 2), + toastDuration: Duration(seconds: durationInSecond), ); } } diff --git a/mobile/lib/shared/views/splash_screen.dart b/mobile/lib/shared/views/splash_screen.dart index ead677582a..b62e5d6b09 100644 --- a/mobile/lib/shared/views/splash_screen.dart +++ b/mobile/lib/shared/views/splash_screen.dart @@ -8,30 +8,34 @@ import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/shared/providers/api.provider.dart'; class SplashScreenPage extends HookConsumerWidget { const SplashScreenPage({Key? key}) : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { + final apiService = ref.watch(apiServiceProvider); HiveSavedLoginInfo? loginInfo = Hive.box(hiveLoginInfoBox).get(savedLoginInfoKey); void performLoggingIn() async { - var isAuthenticated = - await ref.read(authenticationProvider.notifier).login( - loginInfo!.email, - loginInfo.password, - loginInfo.serverUrl, - true, - ); + if (loginInfo != null) { + // Make sure API service is initialized + apiService.setEndpoint(loginInfo.serverUrl); - if (isAuthenticated) { - // Resume backup (if enable) then navigate - ref.watch(backupProvider.notifier).resumeBackup(); - AutoRouter.of(context).replace(const TabControllerRoute()); - } else { - AutoRouter.of(context).replace(const LoginRoute()); + var isSuccess = + await ref.read(authenticationProvider.notifier).setSuccessLoginInfo( + accessToken: loginInfo.accessToken, + isSavedLoginInfo: true, + ); + if (isSuccess) { + // Resume backup (if enable) then navigate + ref.watch(backupProvider.notifier).resumeBackup(); + AutoRouter.of(context).replace(const TabControllerRoute()); + } else { + AutoRouter.of(context).replace(const LoginRoute()); + } } } diff --git a/mobile/openapi/lib/model/user_response_dto.dart b/mobile/openapi/lib/model/user_response_dto.dart index a6242d2c8411dc4e66c27c2fa6043c553fb2308c..63c176378fe413d055c07013c9612d29f6868e7a 100644 GIT binary patch delta 503 zcmcbt{Z3~?4Ko{;0tig5VU~h1r!gzZfCckQGE$55GE)?6Z54o|ni^E?DQ2a~&zZF* z^RQ@4)@RY09KvETxt+yf@*x(r$&XpQCTp{5P7Y!f6IW2sNX<>m%+bq8EY5JwPf68O z&;}YYxtdje@(fm)$(vYZC!b|inf#trLlUSqv)C~uH!}}iwGLYt)McBS*bEpaZ($do ze3o5y@*8&5$-*4UlPx$@C&zObPoBbIz4;=?9wu>peTBs0;?$xN4Gm3&YKWT^^z|pt z<5pt=Qir)SK$I3wh9*#@f nZbmyK2c+he6jkb#CFYcZ%>}y&NWkp0g0n!jLcr#5k!nT&Zy|`{ delta 360 zcmZutyGp}Q043&OjMdtQg;u1um9_~DMG+A!oy1vi6I{~f+ClGIY0;@}sQ^9SMw z2$@6?+}s>QoCG%~-K7r32Tq>lJg4)f-dE29Hb+dI+P>M+EZH|qPbH3TP*)*AfK#@Z z{B|`{Cyhq5B;~S%XSRq*u7nkC3A+)WaI3iF>KJnc6!^H7mT(;LnYe~W zu@NSxt|t<0#gS>` { const loginResponse = await this.oauthService.callback(dto); response.setHeader('Set-Cookie', this.immichJwtService.getCookies(loginResponse, AuthType.OAUTH)); return loginResponse; From 41ffa0c0152e6d7fb9df89296d02c6fa00bc0784 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 20 Nov 2022 13:09:31 -0600 Subject: [PATCH 02/30] fix(server): Server freezes when getting statistic (#994) * fix(server): Server freezes when getting statistic * remove dead code --- mobile/openapi/doc/UsageByUserDto.md | Bin 550 -> 522 bytes .../openapi/lib/model/album_response_dto.dart | Bin 5774 -> 5619 bytes .../openapi/lib/model/asset_response_dto.dart | Bin 9208 -> 9013 bytes .../openapi/lib/model/usage_by_user_dto.dart | Bin 4414 -> 4188 bytes .../openapi/lib/model/user_response_dto.dart | Bin 5742 -> 5587 bytes .../response-dto/server-stats-response.dto.ts | 2 - .../usage-by-user-response.dto.ts | 5 +- .../api-v1/server-info/server-info.service.ts | 81 +++++++----------- server/immich-openapi-specs.json | 2 +- web/src/api/open-api/api.ts | 6 -- .../server-stats/server-stats-panel.svelte | 20 ++--- .../admin-page/server-stats/stats-card.svelte | 4 +- 12 files changed, 46 insertions(+), 74 deletions(-) diff --git a/mobile/openapi/doc/UsageByUserDto.md b/mobile/openapi/doc/UsageByUserDto.md index 2e6a074c0c9031b2858a142693d7050052eac76d..ffdc2a88ecb996ffeb6ff03a48bca7c4e10278ee 100644 GIT binary patch delta 11 ScmZ3+(#102`(|NAVMYKM{sWr; delta 28 jcmeBTS;jKqyI_7&R%&udv6hxXje?d|W?sq0H*$;sml_Ht diff --git a/mobile/openapi/lib/model/album_response_dto.dart b/mobile/openapi/lib/model/album_response_dto.dart index f9901a358f593e78e93c30636b48f1eb7ba6d872..ac170523d856a423cf67a4cfc0a2e12cf137fd2d 100644 GIT binary patch delta 335 zcmeCv{j9xVI`ibY%u4QMwJfS^`6U^tMS7D3I7KHP zVo{p>l0|+pFRSKcYgYToHLU8Bm$J%CKFlg2s*zuwms;eRqL-0aoZ+0GlB%ho%{BQ0 ztNdg^Hfcr;uznXdv&l7V5|SFl8Hq)yDWS!wMa8JY}uI9Y;2bFw{$XYYiT$r52sW`clQ+x9!PAw+E#A2ZC5)BPag=#JZ5SaX#TWzuoPsZeSo(ygU zg<4H(i1;L4vCWdag^ZIA@G5NH#;4E7pHi8Zn46ibkX4+Yr=w6k`5(X7W^sXLW~>rt ogn1biQu9iRD)q_|b4pWn6w-_G%M+7wQmx=Doz3E+<&2D60Bk>K^8f$< delta 483 zcmeyY-KV=@InP-xWTX~pD%8|KFxaoF;E&QJH*&MQ`$77MIEPteTV4Smh=+ zv5HA5C}`xD=cN{Trs!oP7H2r;r=)5sXajAUyp~md@+nqXW}u46{OqEW|FW7*He{2U z?8zoSIgQPN4dR;3^Vl>QC!b_jpZtkkd@?tO;$%Gz)ybh8=O(Li%1?IV)ZARislgO6NQh%)EN)C8(jP^i_khN}T`i&9HUi}Dmo zib_)v;+qff6fyEZm4U-zvLUa+<{&`&DA31i~w6iexLvV diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart index f916088ba26543b3b67794c6c50546debace41d1..164a426dc103d23997d17babf675888cac06d07e 100644 GIT binary patch delta 437 zcmZutO-mbL5GE+N*$>?Wlhj1(*d^Ir6H-V^50;=bReylq#I~#Z;;tlF*^jotc*qEFCJ77FRxOx3liUgv5ml|-xvHS7ozIO38kp^RKoM$dVXW{}zB^|R;RyAC^>R5r(@N8yMS%fEP*p^1HFXiz|dWdW3 z88o?o8F>`1IJ$BT2XY?2<=SWE1zsvOyjPxLPk9p$4_+n;M6cHauf62!VP}Ks%T%Wx zc`g!RP%S`LA7fF~5UE9cQFk$u(!o+DKBm6KM*3%e$_fJJhlXL&Chy^fn`{YM&cUag z!^17JH|xLoER6g%g?@CqfG6Ye2PgeQcBgH%+*RsbQlU+Jr)-=V`_Ol9;hdBN@%Q_k f>RYQBu?nsE-aBih$)@gD1;=BP@${n~iG=tMoU4kE delta 528 zcmdn$_QQR{U3NAu1rV5gmt6|R{LZdCS(HOzvH^$8WOoju$t4_Wlc#cMOy0p^F!?cu z&SWl5t;zbFI+MeIxRz66@&X_}48+ekEhbBHX-u}^(wdye6)|}`m*(VqToNJ*3L2Ry zdKrnu8P54BshSGfKr1J+aVtv#CGyMjQj0t>WbL@+CP#5AOs?Rzn7oc#cJfJXIT?^K z#cqjZ`9+x}sp!W2=Qf*c%p)_|m&assJ&()egFyN-kJ)5(-Xy5EHh1zqW}KYIFD)FO zRh*v}U8J6wq8_VYtB{#u%{6%ezv1M|{ECym@heZ37FaR)sDSF^Hv;ODMFr<<-YmG2 zMO@1GA}VVGg$%T5FMy#llc_Hn1QmJYh;?4@hf8iDyyCRURHMUb$Rj0kqS5L qpmu>`GBvNHs8X*iF{d0DJRuR0meeeZVps@%m@H~*q0js diff --git a/mobile/openapi/lib/model/usage_by_user_dto.dart b/mobile/openapi/lib/model/usage_by_user_dto.dart index 68b33248951277b623dfb1a1a3309aff25892b70..6657b946e35d6ceb3cc0c2a918a3d557267fa7de 100644 GIT binary patch delta 41 zcmV+^0M`G$BHSRbrU8?|0d|u|0?Csx1K5)y1fP@B1iiDf1q%eT_X+$4lL8Sub@&j| delta 148 zcmcbkuuo~jGDi0Nq^#8BlH$pe7`xzXVWvw$P;muYTLqYsYfR7KN>(#(6o87`s=yRP ovFsOyvejc1;2Jk`vP!eCK_n(8aLU6B*(}b<$qwU8mJo0O08HC8E&u=k diff --git a/mobile/openapi/lib/model/user_response_dto.dart b/mobile/openapi/lib/model/user_response_dto.dart index 63c176378fe413d055c07013c9612d29f6868e7a..a6242d2c8411dc4e66c27c2fa6043c553fb2308c 100644 GIT binary patch delta 360 zcmZutyGp}Q043&OjMdtQg;u1um9_~DMG+A!oy1vi6I{~f+ClGIY0;@}sQ^9SMw z2$@6?+}s>QoCG%~-K7r32Tq>lJg4)f-dE29Hb+dI+P>M+EZH|qPbH3TP*)*AfK#@Z z{B|`{Cyhq5B;~S%XSRq*u7nkC3A+)WaI3iF>KJnc6!^H7mT(;LnYe~W zu@NSxt|t<0#gS>`m%+bq8EY5JwPf68O z&;}YYxtdje@(fm)$(vYZC!b|inf#trLlUSqv)C~uH!}}iwGLYt)McBS*bEpaZ($do ze3o5y@*8&5$-*4UlPx$@C&zObPoBbIz4;=?9wu>peTBs0;?$xN4Gm3&YKWT^^z|pt z<5pt=Qir)SK$I3wh9*#@f nZbmyK2c+he6jkb#CFYcZ%>}y&NWkp0g0n!jLcr#5k!nT&Zy|`{ diff --git a/server/apps/immich/src/api-v1/server-info/response-dto/server-stats-response.dto.ts b/server/apps/immich/src/api-v1/server-info/response-dto/server-stats-response.dto.ts index aef4acd118..615acfcf05 100644 --- a/server/apps/immich/src/api-v1/server-info/response-dto/server-stats-response.dto.ts +++ b/server/apps/immich/src/api-v1/server-info/response-dto/server-stats-response.dto.ts @@ -5,7 +5,6 @@ export class ServerStatsResponseDto { constructor() { this.photos = 0; this.videos = 0; - this.objects = 0; this.usageByUser = []; this.usageRaw = 0; this.usage = ''; @@ -34,7 +33,6 @@ export class ServerStatsResponseDto { { photos: 1, videos: 1, - objects: 1, diskUsageRaw: 1, }, ], diff --git a/server/apps/immich/src/api-v1/server-info/response-dto/usage-by-user-response.dto.ts b/server/apps/immich/src/api-v1/server-info/response-dto/usage-by-user-response.dto.ts index 863c77af9d..7502d63afd 100644 --- a/server/apps/immich/src/api-v1/server-info/response-dto/usage-by-user-response.dto.ts +++ b/server/apps/immich/src/api-v1/server-info/response-dto/usage-by-user-response.dto.ts @@ -3,16 +3,15 @@ import { ApiProperty } from '@nestjs/swagger'; export class UsageByUserDto { constructor(userId: string) { this.userId = userId; - this.objects = 0; this.videos = 0; this.photos = 0; + this.usageRaw = 0; + this.usage = '0B'; } @ApiProperty({ type: 'string' }) userId: string; @ApiProperty({ type: 'integer' }) - objects: number; - @ApiProperty({ type: 'integer' }) videos: number; @ApiProperty({ type: 'integer' }) photos: number; diff --git a/server/apps/immich/src/api-v1/server-info/server-info.service.ts b/server/apps/immich/src/api-v1/server-info/server-info.service.ts index f13688e6ec..db85a7038f 100644 --- a/server/apps/immich/src/api-v1/server-info/server-info.service.ts +++ b/server/apps/immich/src/api-v1/server-info/server-info.service.ts @@ -7,8 +7,6 @@ import { UsageByUserDto } from './response-dto/usage-by-user-response.dto'; import { AssetEntity } from '@app/database/entities/asset.entity'; import { Repository } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; -import path from 'path'; -import { readdirSync, statSync } from 'fs'; import { asHumanReadable } from '../../utils/human-readable.util'; @Injectable() @@ -35,59 +33,46 @@ export class ServerInfoService { } async getStats(): Promise { - const res = await this.assetRepository - .createQueryBuilder('asset') - .select(`COUNT(asset.id)`, 'count') - .addSelect(`asset.type`, 'type') - .addSelect(`asset.userId`, 'userId') - .groupBy('asset.type, asset.userId') - .addGroupBy('asset.type') + const serverStats = new ServerStatsResponseDto(); + + type UserStatsQueryResponse = { + assetType: string; + assetCount: string; + totalSizeInBytes: string; + userId: string; + }; + + const userStatsQueryResponse: UserStatsQueryResponse[] = await this.assetRepository + .createQueryBuilder('a') + .select('COUNT(a.id)', 'assetCount') + .addSelect('SUM(ei.fileSizeInByte)', 'totalSizeInBytes') + .addSelect('a."userId"') + .addSelect('a.type', 'assetType') + .where('a.isVisible = true') + .leftJoin('a.exifInfo', 'ei') + .groupBy('a."userId"') + .addGroupBy('a.type') .getRawMany(); - const serverStats = new ServerStatsResponseDto(); const tmpMap = new Map(); const getUsageByUser = (id: string) => tmpMap.get(id) || new UsageByUserDto(id); - res.map((item) => { - const usage: UsageByUserDto = getUsageByUser(item.userId); - if (item.type === 'IMAGE') { - usage.photos = parseInt(item.count); - serverStats.photos += usage.photos; - } else if (item.type === 'VIDEO') { - usage.videos = parseInt(item.count); - serverStats.videos += usage.videos; - } - tmpMap.set(item.userId, usage); + + userStatsQueryResponse.forEach((r) => { + const usageByUser = getUsageByUser(r.userId); + usageByUser.photos += r.assetType === 'IMAGE' ? parseInt(r.assetCount) : 0; + usageByUser.videos += r.assetType === 'VIDEO' ? parseInt(r.assetCount) : 0; + usageByUser.usageRaw += parseInt(r.totalSizeInBytes); + usageByUser.usage = asHumanReadable(usageByUser.usageRaw); + + serverStats.photos += r.assetType === 'IMAGE' ? parseInt(r.assetCount) : 0; + serverStats.videos += r.assetType === 'VIDEO' ? parseInt(r.assetCount) : 0; + serverStats.usageRaw += parseInt(r.totalSizeInBytes); + serverStats.usage = asHumanReadable(serverStats.usageRaw); + tmpMap.set(r.userId, usageByUser); }); - for (const userId of tmpMap.keys()) { - const usage = getUsageByUser(userId); - const userDiskUsage = await ServerInfoService.getDirectoryStats(path.join(APP_UPLOAD_LOCATION, userId)); - usage.usageRaw = userDiskUsage.size; - usage.objects = userDiskUsage.fileCount; - usage.usage = asHumanReadable(usage.usageRaw); - serverStats.usageRaw += usage.usageRaw; - serverStats.objects += usage.objects; - } - serverStats.usage = asHumanReadable(serverStats.usageRaw); serverStats.usageByUser = Array.from(tmpMap.values()); + return serverStats; } - - private static async getDirectoryStats(dirPath: string) { - let size = 0; - let fileCount = 0; - for (const filename of readdirSync(dirPath)) { - const absFilename = path.join(dirPath, filename); - const fileStat = statSync(absFilename); - if (fileStat.isFile()) { - size += fileStat.size; - fileCount += 1; - } else if (fileStat.isDirectory()) { - const subDirStat = await ServerInfoService.getDirectoryStats(absFilename); - size += subDirStat.size; - fileCount += subDirStat.fileCount; - } - } - return { size, fileCount }; - } } diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 66192c92e8..fcc51b3e6d 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -1 +1 @@ -{"openapi":"3.0.0","paths":{"/user":{"get":{"operationId":"getAllUsers","parameters":[{"name":"isAll","required":true,"in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}}}}}},"tags":["User"],"security":[{"bearer":[]}]},"post":{"operationId":"createUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]},"put":{"operationId":"updateUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/info/{userId}":{"get":{"operationId":"getUserById","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"]}},"/user/me":{"get":{"operationId":"getMyUserInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/count":{"get":{"operationId":"getUserCount","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserCountResponseDto"}}}}},"tags":["User"]}},"/user/{userId}":{"delete":{"operationId":"deleteUser","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/{userId}/restore":{"post":{"operationId":"restoreUser","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/profile-image":{"post":{"operationId":"createProfileImage","parameters":[],"requestBody":{"required":true,"description":"A new avatar for the user","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/CreateProfileImageDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProfileImageResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/profile-image/{userId}":{"get":{"operationId":"getProfileImage","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["User"]}},"/asset/upload":{"post":{"operationId":"uploadFile","parameters":[],"requestBody":{"required":true,"description":"Asset Upload Information","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/AssetFileUploadDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetFileUploadResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/download/{assetId}":{"get":{"operationId":"downloadFile","parameters":[{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}},{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/download-library":{"get":{"operationId":"downloadLibrary","parameters":[{"name":"skip","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/file/{assetId}":{"get":{"operationId":"serveFile","parameters":[{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}},{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/thumbnail/{assetId}":{"get":{"operationId":"getAssetThumbnail","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}},{"name":"format","required":false,"in":"query","schema":{"$ref":"#/components/schemas/ThumbnailFormat"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/curated-objects":{"get":{"operationId":"getCuratedObjects","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedObjectsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/curated-locations":{"get":{"operationId":"getCuratedLocations","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedLocationsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search-terms":{"get":{"operationId":"getAssetSearchTerms","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search":{"post":{"operationId":"searchAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchAssetDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/count-by-time-bucket":{"post":{"operationId":"getAssetCountByTimeBucket","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAssetCountByTimeBucketDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCountByTimeBucketResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/count-by-user-id":{"get":{"operationId":"getAssetCountByUserId","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCountByUserIdResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset":{"get":{"operationId":"getAllAssets","summary":"","description":"Get all AssetEntity belong to the user","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DeleteAssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/time-bucket":{"post":{"operationId":"getAssetByTimeBucket","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAssetByTimeBucketDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/{deviceId}":{"get":{"operationId":"getUserAssetsByDeviceId","summary":"","description":"Get all asset of a device that are in the database, ID only.","parameters":[{"name":"deviceId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/assetById/{assetId}":{"get":{"operationId":"getAssetById","summary":"","description":"Get a single asset's information","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]},"put":{"operationId":"updateAssetById","summary":"","description":"Update an asset","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/check":{"post":{"operationId":"checkDuplicateAsset","summary":"","description":"Check duplicated asset before uploading - for Web upload used","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/exist":{"post":{"operationId":"checkExistingAssets","summary":"","description":"Checks if multiple assets exist on the server and returns all existing - used by background backup","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckExistingAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckExistingAssetsResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/auth/login":{"post":{"operationId":"login","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginCredentialDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponseDto"}}}}},"tags":["Authentication"]}},"/auth/admin-sign-up":{"post":{"operationId":"adminSignUp","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignUpDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSignupResponseDto"}}}},"400":{"description":"The server already has an admin"}},"tags":["Authentication"]}},"/auth/validateToken":{"post":{"operationId":"validateAccessToken","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateAccessTokenResponseDto"}}}}},"tags":["Authentication"],"security":[{"bearer":[]}]}},"/auth/logout":{"post":{"operationId":"logout","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogoutResponseDto"}}}}},"tags":["Authentication"]}},"/oauth/config":{"post":{"operationId":"generateConfig","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthConfigDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthConfigResponseDto"}}}}},"tags":["OAuth"]}},"/oauth/callback":{"post":{"operationId":"callback","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthCallbackDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponseDto"}}}}},"tags":["OAuth"]}},"/device-info":{"post":{"operationId":"createDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDeviceInfoDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDeviceInfoDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]}},"/server-info":{"get":{"operationId":"getServerInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerInfoResponseDto"}}}}},"tags":["Server Info"]}},"/server-info/ping":{"get":{"operationId":"pingServer","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerPingResponse"}}}}},"tags":["Server Info"]}},"/server-info/version":{"get":{"operationId":"getServerVersion","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerVersionReponseDto"}}}}},"tags":["Server Info"]}},"/server-info/stats":{"get":{"operationId":"getStats","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerStatsResponseDto"}}}}},"tags":["Server Info"]}},"/album/count-by-user-id":{"get":{"operationId":"getAlbumCountByUserId","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumCountResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album":{"post":{"operationId":"createAlbum","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAlbumDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"get":{"operationId":"getAllAlbums","parameters":[{"name":"shared","required":false,"in":"query","schema":{"type":"boolean"}},{"name":"assetId","required":false,"in":"query","description":"Only returns albums that contain the asset\nIgnores the shared parameter\nundefined: get all albums","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/users":{"put":{"operationId":"addUsersToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddUsersDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/assets":{"put":{"operationId":"addAssetsToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddAssetsResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"removeAssetFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}":{"get":{"operationId":"getAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAlbumDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/user/{userId}":{"delete":{"operationId":"removeUserFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}},{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/download":{"get":{"operationId":"downloadArchive","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}},{"name":"skip","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/jobs":{"get":{"operationId":"getAllJobsStatus","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AllJobStatusResponseDto"}}}}},"tags":["Job"],"security":[{"bearer":[]}]}},"/jobs/{jobId}":{"get":{"operationId":"getJobStatus","parameters":[{"name":"jobId","required":true,"in":"path","schema":{"$ref":"#/components/schemas/JobId"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobStatusResponseDto"}}}}},"tags":["Job"],"security":[{"bearer":[]}]},"put":{"operationId":"sendJobCommand","parameters":[{"name":"jobId","required":true,"in":"path","schema":{"$ref":"#/components/schemas/JobId"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobCommandDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"number"}}}}},"tags":["Job"],"security":[{"bearer":[]}]}},"/system-config":{"get":{"operationId":"getConfig","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemConfigResponseDto"}}}}},"tags":["System Config"],"security":[{"bearer":[]}]},"put":{"operationId":"updateConfig","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSystemConfigDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemConfigResponseDto"}}}}},"tags":["System Config"],"security":[{"bearer":[]}]}}},"info":{"title":"Immich","description":"Immich API","version":"1.17.0","contact":{}},"tags":[],"servers":[{"url":"/api"}],"components":{"securitySchemes":{"bearer":{"scheme":"Bearer","bearerFormat":"JWT","type":"http","name":"JWT","description":"Enter JWT token","in":"header"}},"schemas":{"UserResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"},"profileImagePath":{"type":"string"},"shouldChangePassword":{"type":"boolean"},"isAdmin":{"type":"boolean"},"deletedAt":{"format":"date-time","type":"string","nullable":true}},"required":["id","email","firstName","lastName","createdAt","profileImagePath","shouldChangePassword","isAdmin","deletedAt"]},"CreateUserDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"John"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"UserCountResponseDto":{"type":"object","properties":{"userCount":{"type":"integer"}},"required":["userCount"]},"UpdateUserDto":{"type":"object","properties":{"id":{"type":"string"},"password":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"isAdmin":{"type":"boolean"},"shouldChangePassword":{"type":"boolean"},"profileImagePath":{"type":"string"}},"required":["id"]},"CreateProfileImageDto":{"type":"object","properties":{"file":{"type":"string","format":"binary"}},"required":["file"]},"CreateProfileImageResponseDto":{"type":"object","properties":{"userId":{"type":"string"},"profileImagePath":{"type":"string"}},"required":["userId","profileImagePath"]},"AssetFileUploadDto":{"type":"object","properties":{"assetData":{"type":"string","format":"binary"}},"required":["assetData"]},"AssetFileUploadResponseDto":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]},"ThumbnailFormat":{"type":"string","enum":["JPEG","WEBP"]},"CuratedObjectsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","object","resizePath","deviceAssetId","deviceId"]},"CuratedLocationsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"city":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","city","resizePath","deviceAssetId","deviceId"]},"SearchAssetDto":{"type":"object","properties":{"searchTerm":{"type":"string"}},"required":["searchTerm"]},"AssetTypeEnum":{"type":"string","enum":["IMAGE","VIDEO","AUDIO","OTHER"]},"ExifResponseDto":{"type":"object","properties":{"id":{"type":"integer","nullable":true,"default":null,"format":"int64"},"fileSizeInByte":{"type":"integer","nullable":true,"default":null,"format":"int64"},"make":{"type":"string","nullable":true,"default":null},"model":{"type":"string","nullable":true,"default":null},"imageName":{"type":"string","nullable":true,"default":null},"exifImageWidth":{"type":"number","nullable":true,"default":null},"exifImageHeight":{"type":"number","nullable":true,"default":null},"orientation":{"type":"string","nullable":true,"default":null},"dateTimeOriginal":{"format":"date-time","type":"string","nullable":true,"default":null},"modifyDate":{"format":"date-time","type":"string","nullable":true,"default":null},"lensModel":{"type":"string","nullable":true,"default":null},"fNumber":{"type":"number","nullable":true,"default":null},"focalLength":{"type":"number","nullable":true,"default":null},"iso":{"type":"number","nullable":true,"default":null},"exposureTime":{"type":"number","nullable":true,"default":null},"latitude":{"type":"number","nullable":true,"default":null},"longitude":{"type":"number","nullable":true,"default":null},"city":{"type":"string","nullable":true,"default":null},"state":{"type":"string","nullable":true,"default":null},"country":{"type":"string","nullable":true,"default":null}}},"SmartInfoResponseDto":{"type":"object","properties":{"id":{"type":"string"},"tags":{"nullable":true,"type":"array","items":{"type":"string"}},"objects":{"nullable":true,"type":"array","items":{"type":"string"}}}},"AssetResponseDto":{"type":"object","properties":{"type":{"$ref":"#/components/schemas/AssetTypeEnum"},"id":{"type":"string"},"deviceAssetId":{"type":"string"},"ownerId":{"type":"string"},"deviceId":{"type":"string"},"originalPath":{"type":"string"},"resizePath":{"type":"string","nullable":true},"createdAt":{"type":"string"},"modifiedAt":{"type":"string"},"isFavorite":{"type":"boolean"},"mimeType":{"type":"string","nullable":true},"duration":{"type":"string"},"webpPath":{"type":"string","nullable":true},"encodedVideoPath":{"type":"string","nullable":true},"exifInfo":{"$ref":"#/components/schemas/ExifResponseDto"},"smartInfo":{"$ref":"#/components/schemas/SmartInfoResponseDto"},"livePhotoVideoId":{"type":"string","nullable":true}},"required":["type","id","deviceAssetId","ownerId","deviceId","originalPath","resizePath","createdAt","modifiedAt","isFavorite","mimeType","duration","webpPath","encodedVideoPath","livePhotoVideoId"]},"TimeGroupEnum":{"type":"string","enum":["day","month"]},"GetAssetCountByTimeBucketDto":{"type":"object","properties":{"timeGroup":{"$ref":"#/components/schemas/TimeGroupEnum"}},"required":["timeGroup"]},"AssetCountByTimeBucket":{"type":"object","properties":{"timeBucket":{"type":"string"},"count":{"type":"integer"}},"required":["timeBucket","count"]},"AssetCountByTimeBucketResponseDto":{"type":"object","properties":{"totalCount":{"type":"integer"},"buckets":{"type":"array","items":{"$ref":"#/components/schemas/AssetCountByTimeBucket"}}},"required":["totalCount","buckets"]},"AssetCountByUserIdResponseDto":{"type":"object","properties":{"audio":{"type":"integer","default":0},"photos":{"type":"integer","default":0},"videos":{"type":"integer","default":0},"other":{"type":"integer","default":0},"total":{"type":"integer","default":0}},"required":["audio","photos","videos","other","total"]},"GetAssetByTimeBucketDto":{"type":"object","properties":{"timeBucket":{"title":"Array of date time buckets","example":["2015-06-01T00:00:00.000Z","2016-02-01T00:00:00.000Z","2016-03-01T00:00:00.000Z"],"type":"array","items":{"type":"string"}}},"required":["timeBucket"]},"UpdateAssetDto":{"type":"object","properties":{"isFavorite":{"type":"boolean"}},"required":["isFavorite"]},"DeleteAssetDto":{"type":"object","properties":{"ids":{"title":"Array of asset IDs to delete","example":["bf973405-3f2a-48d2-a687-2ed4167164be","dd41870b-5d00-46d2-924e-1d8489a0aa0f","fad77c3f-deef-4e7e-9608-14c1aa4e559a"],"type":"array","items":{"type":"string"}}},"required":["ids"]},"DeleteAssetStatus":{"type":"string","enum":["SUCCESS","FAILED"]},"DeleteAssetResponseDto":{"type":"object","properties":{"status":{"$ref":"#/components/schemas/DeleteAssetStatus"},"id":{"type":"string"}},"required":["status","id"]},"CheckDuplicateAssetDto":{"type":"object","properties":{"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["deviceAssetId","deviceId"]},"CheckDuplicateAssetResponseDto":{"type":"object","properties":{"isExist":{"type":"boolean"},"id":{"type":"string"}},"required":["isExist"]},"CheckExistingAssetsDto":{"type":"object","properties":{"deviceAssetIds":{"type":"array","items":{"type":"string"}},"deviceId":{"type":"string"}},"required":["deviceAssetIds","deviceId"]},"CheckExistingAssetsResponseDto":{"type":"object","properties":{"existingIds":{"type":"array","items":{"type":"string"}}},"required":["existingIds"]},"LoginCredentialDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"}},"required":["email","password"]},"LoginResponseDto":{"type":"object","properties":{"accessToken":{"type":"string","readOnly":true},"userId":{"type":"string","readOnly":true},"userEmail":{"type":"string","readOnly":true},"firstName":{"type":"string","readOnly":true},"lastName":{"type":"string","readOnly":true},"profileImagePath":{"type":"string","readOnly":true},"isAdmin":{"type":"boolean","readOnly":true},"shouldChangePassword":{"type":"boolean","readOnly":true}},"required":["accessToken","userId","userEmail","firstName","lastName","profileImagePath","isAdmin","shouldChangePassword"]},"SignUpDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"Admin"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"AdminSignupResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"}},"required":["id","email","firstName","lastName","createdAt"]},"ValidateAccessTokenResponseDto":{"type":"object","properties":{"authStatus":{"type":"boolean"}},"required":["authStatus"]},"LogoutResponseDto":{"type":"object","properties":{"successful":{"type":"boolean","readOnly":true},"redirectUri":{"type":"string","readOnly":true}},"required":["successful","redirectUri"]},"OAuthConfigDto":{"type":"object","properties":{"redirectUri":{"type":"string"}},"required":["redirectUri"]},"OAuthConfigResponseDto":{"type":"object","properties":{"enabled":{"type":"boolean","readOnly":true},"url":{"type":"string","readOnly":true},"buttonText":{"type":"string","readOnly":true}},"required":["enabled"]},"OAuthCallbackDto":{"type":"object","properties":{"url":{"type":"string"}},"required":["url"]},"DeviceTypeEnum":{"type":"string","enum":["IOS","ANDROID","WEB"]},"CreateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"DeviceInfoResponseDto":{"type":"object","properties":{"id":{"type":"integer"},"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"userId":{"type":"string"},"deviceId":{"type":"string"},"createdAt":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["id","deviceType","userId","deviceId","createdAt","isAutoBackup"]},"UpdateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"ServerInfoResponseDto":{"type":"object","properties":{"diskSizeRaw":{"type":"integer","format":"int64"},"diskUseRaw":{"type":"integer","format":"int64"},"diskAvailableRaw":{"type":"integer","format":"int64"},"diskUsagePercentage":{"type":"number","format":"float"},"diskSize":{"type":"string"},"diskUse":{"type":"string"},"diskAvailable":{"type":"string"}},"required":["diskSizeRaw","diskUseRaw","diskAvailableRaw","diskUsagePercentage","diskSize","diskUse","diskAvailable"]},"ServerPingResponse":{"type":"object","properties":{"res":{"type":"string","readOnly":true,"example":"pong"}},"required":["res"]},"ServerVersionReponseDto":{"type":"object","properties":{"major":{"type":"integer"},"minor":{"type":"integer"},"patch":{"type":"integer"},"build":{"type":"integer"}},"required":["major","minor","patch","build"]},"UsageByUserDto":{"type":"object","properties":{"userId":{"type":"string"},"objects":{"type":"integer"},"videos":{"type":"integer"},"photos":{"type":"integer"},"usageRaw":{"type":"integer","format":"int64"},"usage":{"type":"string"}},"required":["userId","objects","videos","photos","usageRaw","usage"]},"ServerStatsResponseDto":{"type":"object","properties":{"photos":{"type":"integer"},"videos":{"type":"integer"},"objects":{"type":"integer"},"usageRaw":{"type":"integer","format":"int64"},"usage":{"type":"string"},"usageByUser":{"title":"Array of usage for each user","example":[{"photos":1,"videos":1,"objects":1,"diskUsageRaw":1}],"type":"array","items":{"$ref":"#/components/schemas/UsageByUserDto"}}},"required":["photos","videos","objects","usageRaw","usage","usageByUser"]},"AlbumCountResponseDto":{"type":"object","properties":{"owned":{"type":"integer"},"shared":{"type":"integer"},"sharing":{"type":"integer"}},"required":["owned","shared","sharing"]},"CreateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"sharedWithUserIds":{"type":"array","items":{"type":"string"}},"assetIds":{"type":"array","items":{"type":"string"}}},"required":["albumName"]},"AlbumResponseDto":{"type":"object","properties":{"assetCount":{"type":"integer"},"id":{"type":"string"},"ownerId":{"type":"string"},"albumName":{"type":"string"},"createdAt":{"type":"string"},"albumThumbnailAssetId":{"type":"string","nullable":true},"shared":{"type":"boolean"},"sharedUsers":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}},"assets":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}},"required":["assetCount","id","ownerId","albumName","createdAt","albumThumbnailAssetId","shared","sharedUsers","assets"]},"AddUsersDto":{"type":"object","properties":{"sharedUserIds":{"type":"array","items":{"type":"string"}}},"required":["sharedUserIds"]},"AddAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"AddAssetsResponseDto":{"type":"object","properties":{"successfullyAdded":{"type":"integer"},"alreadyInAlbum":{"type":"array","items":{"type":"string"}},"album":{"$ref":"#/components/schemas/AlbumResponseDto"}},"required":["successfullyAdded","alreadyInAlbum"]},"RemoveAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"UpdateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"albumThumbnailAssetId":{"type":"string"}}},"JobCounts":{"type":"object","properties":{"active":{"type":"integer"},"completed":{"type":"integer"},"failed":{"type":"integer"},"delayed":{"type":"integer"},"waiting":{"type":"integer"}},"required":["active","completed","failed","delayed","waiting"]},"AllJobStatusResponseDto":{"type":"object","properties":{"thumbnailGenerationQueueCount":{"$ref":"#/components/schemas/JobCounts"},"metadataExtractionQueueCount":{"$ref":"#/components/schemas/JobCounts"},"videoConversionQueueCount":{"$ref":"#/components/schemas/JobCounts"},"machineLearningQueueCount":{"$ref":"#/components/schemas/JobCounts"},"isThumbnailGenerationActive":{"type":"boolean"},"isMetadataExtractionActive":{"type":"boolean"},"isVideoConversionActive":{"type":"boolean"},"isMachineLearningActive":{"type":"boolean"}},"required":["thumbnailGenerationQueueCount","metadataExtractionQueueCount","videoConversionQueueCount","machineLearningQueueCount","isThumbnailGenerationActive","isMetadataExtractionActive","isVideoConversionActive","isMachineLearningActive"]},"JobId":{"type":"string","enum":["thumbnail-generation","metadata-extraction","video-conversion","machine-learning"]},"JobStatusResponseDto":{"type":"object","properties":{"isActive":{"type":"boolean"},"queueCount":{"type":"object"}},"required":["isActive","queueCount"]},"JobCommand":{"type":"string","enum":["start","stop"]},"JobCommandDto":{"type":"object","properties":{"command":{"$ref":"#/components/schemas/JobCommand"}},"required":["command"]},"SystemConfigKey":{"type":"string","enum":["ffmpeg_crf","ffmpeg_preset","ffmpeg_target_video_codec","ffmpeg_target_audio_codec","ffmpeg_target_scaling"]},"SystemConfigResponseItem":{"type":"object","properties":{"name":{"type":"string"},"key":{"$ref":"#/components/schemas/SystemConfigKey"},"value":{"type":"string"},"defaultValue":{"type":"string"}},"required":["name","key","value","defaultValue"]},"SystemConfigResponseDto":{"type":"object","properties":{"config":{"type":"array","items":{"$ref":"#/components/schemas/SystemConfigResponseItem"}}},"required":["config"]},"UpdateSystemConfigDto":{"type":"object","properties":{}}}}} \ No newline at end of file +{"openapi":"3.0.0","paths":{"/user":{"get":{"operationId":"getAllUsers","parameters":[{"name":"isAll","required":true,"in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}}}}}},"tags":["User"],"security":[{"bearer":[]}]},"post":{"operationId":"createUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]},"put":{"operationId":"updateUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/info/{userId}":{"get":{"operationId":"getUserById","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"]}},"/user/me":{"get":{"operationId":"getMyUserInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/count":{"get":{"operationId":"getUserCount","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserCountResponseDto"}}}}},"tags":["User"]}},"/user/{userId}":{"delete":{"operationId":"deleteUser","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/{userId}/restore":{"post":{"operationId":"restoreUser","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/profile-image":{"post":{"operationId":"createProfileImage","parameters":[],"requestBody":{"required":true,"description":"A new avatar for the user","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/CreateProfileImageDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProfileImageResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/profile-image/{userId}":{"get":{"operationId":"getProfileImage","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["User"]}},"/asset/upload":{"post":{"operationId":"uploadFile","parameters":[],"requestBody":{"required":true,"description":"Asset Upload Information","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/AssetFileUploadDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetFileUploadResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/download/{assetId}":{"get":{"operationId":"downloadFile","parameters":[{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}},{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/download-library":{"get":{"operationId":"downloadLibrary","parameters":[{"name":"skip","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/file/{assetId}":{"get":{"operationId":"serveFile","parameters":[{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}},{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/thumbnail/{assetId}":{"get":{"operationId":"getAssetThumbnail","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}},{"name":"format","required":false,"in":"query","schema":{"$ref":"#/components/schemas/ThumbnailFormat"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/curated-objects":{"get":{"operationId":"getCuratedObjects","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedObjectsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/curated-locations":{"get":{"operationId":"getCuratedLocations","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedLocationsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search-terms":{"get":{"operationId":"getAssetSearchTerms","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search":{"post":{"operationId":"searchAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchAssetDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/count-by-time-bucket":{"post":{"operationId":"getAssetCountByTimeBucket","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAssetCountByTimeBucketDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCountByTimeBucketResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/count-by-user-id":{"get":{"operationId":"getAssetCountByUserId","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCountByUserIdResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset":{"get":{"operationId":"getAllAssets","summary":"","description":"Get all AssetEntity belong to the user","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DeleteAssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/time-bucket":{"post":{"operationId":"getAssetByTimeBucket","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAssetByTimeBucketDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/{deviceId}":{"get":{"operationId":"getUserAssetsByDeviceId","summary":"","description":"Get all asset of a device that are in the database, ID only.","parameters":[{"name":"deviceId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/assetById/{assetId}":{"get":{"operationId":"getAssetById","summary":"","description":"Get a single asset's information","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]},"put":{"operationId":"updateAssetById","summary":"","description":"Update an asset","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/check":{"post":{"operationId":"checkDuplicateAsset","summary":"","description":"Check duplicated asset before uploading - for Web upload used","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/exist":{"post":{"operationId":"checkExistingAssets","summary":"","description":"Checks if multiple assets exist on the server and returns all existing - used by background backup","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckExistingAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckExistingAssetsResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/auth/login":{"post":{"operationId":"login","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginCredentialDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponseDto"}}}}},"tags":["Authentication"]}},"/auth/admin-sign-up":{"post":{"operationId":"adminSignUp","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignUpDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSignupResponseDto"}}}},"400":{"description":"The server already has an admin"}},"tags":["Authentication"]}},"/auth/validateToken":{"post":{"operationId":"validateAccessToken","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateAccessTokenResponseDto"}}}}},"tags":["Authentication"],"security":[{"bearer":[]}]}},"/auth/logout":{"post":{"operationId":"logout","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogoutResponseDto"}}}}},"tags":["Authentication"]}},"/oauth/config":{"post":{"operationId":"generateConfig","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthConfigDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthConfigResponseDto"}}}}},"tags":["OAuth"]}},"/oauth/callback":{"post":{"operationId":"callback","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthCallbackDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponseDto"}}}}},"tags":["OAuth"]}},"/device-info":{"post":{"operationId":"createDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDeviceInfoDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDeviceInfoDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]}},"/server-info":{"get":{"operationId":"getServerInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerInfoResponseDto"}}}}},"tags":["Server Info"]}},"/server-info/ping":{"get":{"operationId":"pingServer","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerPingResponse"}}}}},"tags":["Server Info"]}},"/server-info/version":{"get":{"operationId":"getServerVersion","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerVersionReponseDto"}}}}},"tags":["Server Info"]}},"/server-info/stats":{"get":{"operationId":"getStats","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerStatsResponseDto"}}}}},"tags":["Server Info"]}},"/album/count-by-user-id":{"get":{"operationId":"getAlbumCountByUserId","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumCountResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album":{"post":{"operationId":"createAlbum","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAlbumDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"get":{"operationId":"getAllAlbums","parameters":[{"name":"shared","required":false,"in":"query","schema":{"type":"boolean"}},{"name":"assetId","required":false,"in":"query","description":"Only returns albums that contain the asset\nIgnores the shared parameter\nundefined: get all albums","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/users":{"put":{"operationId":"addUsersToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddUsersDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/assets":{"put":{"operationId":"addAssetsToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddAssetsResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"removeAssetFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}":{"get":{"operationId":"getAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAlbumDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/user/{userId}":{"delete":{"operationId":"removeUserFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}},{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/download":{"get":{"operationId":"downloadArchive","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}},{"name":"skip","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/jobs":{"get":{"operationId":"getAllJobsStatus","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AllJobStatusResponseDto"}}}}},"tags":["Job"],"security":[{"bearer":[]}]}},"/jobs/{jobId}":{"get":{"operationId":"getJobStatus","parameters":[{"name":"jobId","required":true,"in":"path","schema":{"$ref":"#/components/schemas/JobId"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobStatusResponseDto"}}}}},"tags":["Job"],"security":[{"bearer":[]}]},"put":{"operationId":"sendJobCommand","parameters":[{"name":"jobId","required":true,"in":"path","schema":{"$ref":"#/components/schemas/JobId"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobCommandDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"number"}}}}},"tags":["Job"],"security":[{"bearer":[]}]}},"/system-config":{"get":{"operationId":"getConfig","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemConfigResponseDto"}}}}},"tags":["System Config"],"security":[{"bearer":[]}]},"put":{"operationId":"updateConfig","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSystemConfigDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemConfigResponseDto"}}}}},"tags":["System Config"],"security":[{"bearer":[]}]}}},"info":{"title":"Immich","description":"Immich API","version":"1.17.0","contact":{}},"tags":[],"servers":[{"url":"/api"}],"components":{"securitySchemes":{"bearer":{"scheme":"Bearer","bearerFormat":"JWT","type":"http","name":"JWT","description":"Enter JWT token","in":"header"}},"schemas":{"UserResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"},"profileImagePath":{"type":"string"},"shouldChangePassword":{"type":"boolean"},"isAdmin":{"type":"boolean"},"deletedAt":{"format":"date-time","type":"string","nullable":true}},"required":["id","email","firstName","lastName","createdAt","profileImagePath","shouldChangePassword","isAdmin","deletedAt"]},"CreateUserDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"John"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"UserCountResponseDto":{"type":"object","properties":{"userCount":{"type":"integer"}},"required":["userCount"]},"UpdateUserDto":{"type":"object","properties":{"id":{"type":"string"},"password":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"isAdmin":{"type":"boolean"},"shouldChangePassword":{"type":"boolean"},"profileImagePath":{"type":"string"}},"required":["id"]},"CreateProfileImageDto":{"type":"object","properties":{"file":{"type":"string","format":"binary"}},"required":["file"]},"CreateProfileImageResponseDto":{"type":"object","properties":{"userId":{"type":"string"},"profileImagePath":{"type":"string"}},"required":["userId","profileImagePath"]},"AssetFileUploadDto":{"type":"object","properties":{"assetData":{"type":"string","format":"binary"}},"required":["assetData"]},"AssetFileUploadResponseDto":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]},"ThumbnailFormat":{"type":"string","enum":["JPEG","WEBP"]},"CuratedObjectsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","object","resizePath","deviceAssetId","deviceId"]},"CuratedLocationsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"city":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","city","resizePath","deviceAssetId","deviceId"]},"SearchAssetDto":{"type":"object","properties":{"searchTerm":{"type":"string"}},"required":["searchTerm"]},"AssetTypeEnum":{"type":"string","enum":["IMAGE","VIDEO","AUDIO","OTHER"]},"ExifResponseDto":{"type":"object","properties":{"id":{"type":"integer","nullable":true,"default":null,"format":"int64"},"fileSizeInByte":{"type":"integer","nullable":true,"default":null,"format":"int64"},"make":{"type":"string","nullable":true,"default":null},"model":{"type":"string","nullable":true,"default":null},"imageName":{"type":"string","nullable":true,"default":null},"exifImageWidth":{"type":"number","nullable":true,"default":null},"exifImageHeight":{"type":"number","nullable":true,"default":null},"orientation":{"type":"string","nullable":true,"default":null},"dateTimeOriginal":{"format":"date-time","type":"string","nullable":true,"default":null},"modifyDate":{"format":"date-time","type":"string","nullable":true,"default":null},"lensModel":{"type":"string","nullable":true,"default":null},"fNumber":{"type":"number","nullable":true,"default":null},"focalLength":{"type":"number","nullable":true,"default":null},"iso":{"type":"number","nullable":true,"default":null},"exposureTime":{"type":"number","nullable":true,"default":null},"latitude":{"type":"number","nullable":true,"default":null},"longitude":{"type":"number","nullable":true,"default":null},"city":{"type":"string","nullable":true,"default":null},"state":{"type":"string","nullable":true,"default":null},"country":{"type":"string","nullable":true,"default":null}}},"SmartInfoResponseDto":{"type":"object","properties":{"id":{"type":"string"},"tags":{"nullable":true,"type":"array","items":{"type":"string"}},"objects":{"nullable":true,"type":"array","items":{"type":"string"}}}},"AssetResponseDto":{"type":"object","properties":{"type":{"$ref":"#/components/schemas/AssetTypeEnum"},"id":{"type":"string"},"deviceAssetId":{"type":"string"},"ownerId":{"type":"string"},"deviceId":{"type":"string"},"originalPath":{"type":"string"},"resizePath":{"type":"string","nullable":true},"createdAt":{"type":"string"},"modifiedAt":{"type":"string"},"isFavorite":{"type":"boolean"},"mimeType":{"type":"string","nullable":true},"duration":{"type":"string"},"webpPath":{"type":"string","nullable":true},"encodedVideoPath":{"type":"string","nullable":true},"exifInfo":{"$ref":"#/components/schemas/ExifResponseDto"},"smartInfo":{"$ref":"#/components/schemas/SmartInfoResponseDto"},"livePhotoVideoId":{"type":"string","nullable":true}},"required":["type","id","deviceAssetId","ownerId","deviceId","originalPath","resizePath","createdAt","modifiedAt","isFavorite","mimeType","duration","webpPath","encodedVideoPath","livePhotoVideoId"]},"TimeGroupEnum":{"type":"string","enum":["day","month"]},"GetAssetCountByTimeBucketDto":{"type":"object","properties":{"timeGroup":{"$ref":"#/components/schemas/TimeGroupEnum"}},"required":["timeGroup"]},"AssetCountByTimeBucket":{"type":"object","properties":{"timeBucket":{"type":"string"},"count":{"type":"integer"}},"required":["timeBucket","count"]},"AssetCountByTimeBucketResponseDto":{"type":"object","properties":{"totalCount":{"type":"integer"},"buckets":{"type":"array","items":{"$ref":"#/components/schemas/AssetCountByTimeBucket"}}},"required":["totalCount","buckets"]},"AssetCountByUserIdResponseDto":{"type":"object","properties":{"audio":{"type":"integer","default":0},"photos":{"type":"integer","default":0},"videos":{"type":"integer","default":0},"other":{"type":"integer","default":0},"total":{"type":"integer","default":0}},"required":["audio","photos","videos","other","total"]},"GetAssetByTimeBucketDto":{"type":"object","properties":{"timeBucket":{"title":"Array of date time buckets","example":["2015-06-01T00:00:00.000Z","2016-02-01T00:00:00.000Z","2016-03-01T00:00:00.000Z"],"type":"array","items":{"type":"string"}}},"required":["timeBucket"]},"UpdateAssetDto":{"type":"object","properties":{"isFavorite":{"type":"boolean"}},"required":["isFavorite"]},"DeleteAssetDto":{"type":"object","properties":{"ids":{"title":"Array of asset IDs to delete","example":["bf973405-3f2a-48d2-a687-2ed4167164be","dd41870b-5d00-46d2-924e-1d8489a0aa0f","fad77c3f-deef-4e7e-9608-14c1aa4e559a"],"type":"array","items":{"type":"string"}}},"required":["ids"]},"DeleteAssetStatus":{"type":"string","enum":["SUCCESS","FAILED"]},"DeleteAssetResponseDto":{"type":"object","properties":{"status":{"$ref":"#/components/schemas/DeleteAssetStatus"},"id":{"type":"string"}},"required":["status","id"]},"CheckDuplicateAssetDto":{"type":"object","properties":{"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["deviceAssetId","deviceId"]},"CheckDuplicateAssetResponseDto":{"type":"object","properties":{"isExist":{"type":"boolean"},"id":{"type":"string"}},"required":["isExist"]},"CheckExistingAssetsDto":{"type":"object","properties":{"deviceAssetIds":{"type":"array","items":{"type":"string"}},"deviceId":{"type":"string"}},"required":["deviceAssetIds","deviceId"]},"CheckExistingAssetsResponseDto":{"type":"object","properties":{"existingIds":{"type":"array","items":{"type":"string"}}},"required":["existingIds"]},"LoginCredentialDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"}},"required":["email","password"]},"LoginResponseDto":{"type":"object","properties":{"accessToken":{"type":"string","readOnly":true},"userId":{"type":"string","readOnly":true},"userEmail":{"type":"string","readOnly":true},"firstName":{"type":"string","readOnly":true},"lastName":{"type":"string","readOnly":true},"profileImagePath":{"type":"string","readOnly":true},"isAdmin":{"type":"boolean","readOnly":true},"shouldChangePassword":{"type":"boolean","readOnly":true}},"required":["accessToken","userId","userEmail","firstName","lastName","profileImagePath","isAdmin","shouldChangePassword"]},"SignUpDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"Admin"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"AdminSignupResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"}},"required":["id","email","firstName","lastName","createdAt"]},"ValidateAccessTokenResponseDto":{"type":"object","properties":{"authStatus":{"type":"boolean"}},"required":["authStatus"]},"LogoutResponseDto":{"type":"object","properties":{"successful":{"type":"boolean","readOnly":true},"redirectUri":{"type":"string","readOnly":true}},"required":["successful","redirectUri"]},"OAuthConfigDto":{"type":"object","properties":{"redirectUri":{"type":"string"}},"required":["redirectUri"]},"OAuthConfigResponseDto":{"type":"object","properties":{"enabled":{"type":"boolean","readOnly":true},"url":{"type":"string","readOnly":true},"buttonText":{"type":"string","readOnly":true}},"required":["enabled"]},"OAuthCallbackDto":{"type":"object","properties":{"url":{"type":"string"}},"required":["url"]},"DeviceTypeEnum":{"type":"string","enum":["IOS","ANDROID","WEB"]},"CreateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"DeviceInfoResponseDto":{"type":"object","properties":{"id":{"type":"integer"},"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"userId":{"type":"string"},"deviceId":{"type":"string"},"createdAt":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["id","deviceType","userId","deviceId","createdAt","isAutoBackup"]},"UpdateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"ServerInfoResponseDto":{"type":"object","properties":{"diskSizeRaw":{"type":"integer","format":"int64"},"diskUseRaw":{"type":"integer","format":"int64"},"diskAvailableRaw":{"type":"integer","format":"int64"},"diskUsagePercentage":{"type":"number","format":"float"},"diskSize":{"type":"string"},"diskUse":{"type":"string"},"diskAvailable":{"type":"string"}},"required":["diskSizeRaw","diskUseRaw","diskAvailableRaw","diskUsagePercentage","diskSize","diskUse","diskAvailable"]},"ServerPingResponse":{"type":"object","properties":{"res":{"type":"string","readOnly":true,"example":"pong"}},"required":["res"]},"ServerVersionReponseDto":{"type":"object","properties":{"major":{"type":"integer"},"minor":{"type":"integer"},"patch":{"type":"integer"},"build":{"type":"integer"}},"required":["major","minor","patch","build"]},"UsageByUserDto":{"type":"object","properties":{"userId":{"type":"string"},"videos":{"type":"integer"},"photos":{"type":"integer"},"usageRaw":{"type":"integer","format":"int64"},"usage":{"type":"string"}},"required":["userId","videos","photos","usageRaw","usage"]},"ServerStatsResponseDto":{"type":"object","properties":{"photos":{"type":"integer"},"videos":{"type":"integer"},"objects":{"type":"integer"},"usageRaw":{"type":"integer","format":"int64"},"usage":{"type":"string"},"usageByUser":{"title":"Array of usage for each user","example":[{"photos":1,"videos":1,"diskUsageRaw":1}],"type":"array","items":{"$ref":"#/components/schemas/UsageByUserDto"}}},"required":["photos","videos","objects","usageRaw","usage","usageByUser"]},"AlbumCountResponseDto":{"type":"object","properties":{"owned":{"type":"integer"},"shared":{"type":"integer"},"sharing":{"type":"integer"}},"required":["owned","shared","sharing"]},"CreateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"sharedWithUserIds":{"type":"array","items":{"type":"string"}},"assetIds":{"type":"array","items":{"type":"string"}}},"required":["albumName"]},"AlbumResponseDto":{"type":"object","properties":{"assetCount":{"type":"integer"},"id":{"type":"string"},"ownerId":{"type":"string"},"albumName":{"type":"string"},"createdAt":{"type":"string"},"albumThumbnailAssetId":{"type":"string","nullable":true},"shared":{"type":"boolean"},"sharedUsers":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}},"assets":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}},"required":["assetCount","id","ownerId","albumName","createdAt","albumThumbnailAssetId","shared","sharedUsers","assets"]},"AddUsersDto":{"type":"object","properties":{"sharedUserIds":{"type":"array","items":{"type":"string"}}},"required":["sharedUserIds"]},"AddAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"AddAssetsResponseDto":{"type":"object","properties":{"successfullyAdded":{"type":"integer"},"alreadyInAlbum":{"type":"array","items":{"type":"string"}},"album":{"$ref":"#/components/schemas/AlbumResponseDto"}},"required":["successfullyAdded","alreadyInAlbum"]},"RemoveAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"UpdateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"albumThumbnailAssetId":{"type":"string"}}},"JobCounts":{"type":"object","properties":{"active":{"type":"integer"},"completed":{"type":"integer"},"failed":{"type":"integer"},"delayed":{"type":"integer"},"waiting":{"type":"integer"}},"required":["active","completed","failed","delayed","waiting"]},"AllJobStatusResponseDto":{"type":"object","properties":{"thumbnailGenerationQueueCount":{"$ref":"#/components/schemas/JobCounts"},"metadataExtractionQueueCount":{"$ref":"#/components/schemas/JobCounts"},"videoConversionQueueCount":{"$ref":"#/components/schemas/JobCounts"},"machineLearningQueueCount":{"$ref":"#/components/schemas/JobCounts"},"isThumbnailGenerationActive":{"type":"boolean"},"isMetadataExtractionActive":{"type":"boolean"},"isVideoConversionActive":{"type":"boolean"},"isMachineLearningActive":{"type":"boolean"}},"required":["thumbnailGenerationQueueCount","metadataExtractionQueueCount","videoConversionQueueCount","machineLearningQueueCount","isThumbnailGenerationActive","isMetadataExtractionActive","isVideoConversionActive","isMachineLearningActive"]},"JobId":{"type":"string","enum":["thumbnail-generation","metadata-extraction","video-conversion","machine-learning"]},"JobStatusResponseDto":{"type":"object","properties":{"isActive":{"type":"boolean"},"queueCount":{"type":"object"}},"required":["isActive","queueCount"]},"JobCommand":{"type":"string","enum":["start","stop"]},"JobCommandDto":{"type":"object","properties":{"command":{"$ref":"#/components/schemas/JobCommand"}},"required":["command"]},"SystemConfigKey":{"type":"string","enum":["ffmpeg_crf","ffmpeg_preset","ffmpeg_target_video_codec","ffmpeg_target_audio_codec","ffmpeg_target_scaling"]},"SystemConfigResponseItem":{"type":"object","properties":{"name":{"type":"string"},"key":{"$ref":"#/components/schemas/SystemConfigKey"},"value":{"type":"string"},"defaultValue":{"type":"string"}},"required":["name","key","value","defaultValue"]},"SystemConfigResponseDto":{"type":"object","properties":{"config":{"type":"array","items":{"$ref":"#/components/schemas/SystemConfigResponseItem"}}},"required":["config"]},"UpdateSystemConfigDto":{"type":"object","properties":{}}}}} \ No newline at end of file diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 98539841fa..39bd30a2fe 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -1632,12 +1632,6 @@ export interface UsageByUserDto { * @memberof UsageByUserDto */ 'userId': string; - /** - * - * @type {number} - * @memberof UsageByUserDto - */ - 'objects': number; /** * * @type {number} diff --git a/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte b/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte index b5269b0bee..bfae8451cc 100644 --- a/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte +++ b/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte @@ -2,7 +2,6 @@ import { ServerStatsResponseDto, UserResponseDto } from '@api'; import CameraIris from 'svelte-material-icons/CameraIris.svelte'; import PlayCircle from 'svelte-material-icons/PlayCircle.svelte'; - import FileImageOutline from 'svelte-material-icons/FileImageOutline.svelte'; import Memory from 'svelte-material-icons/Memory.svelte'; import StatsCard from './stats-card.svelte'; export let stats: ServerStatsResponseDto; @@ -27,7 +26,6 @@
-
@@ -39,11 +37,10 @@ class="border rounded-md mb-4 bg-gray-50 dark:bg-immich-dark-gray dark:border-immich-dark-gray flex text-immich-primary dark:text-immich-dark-primary w-full h-12" > - User - Photos - Videos - Objects - Size + User + Photos + Videos + Size - {getFullName(user.userId)} - {user.photos} - {user.videos} - {user.objects} - {user.usage} + {getFullName(user.userId)} + {user.photos} + {user.videos} + {user.usage} {/each} diff --git a/web/src/lib/components/admin-page/server-stats/stats-card.svelte b/web/src/lib/components/admin-page/server-stats/stats-card.svelte index 5af64d38bc..e60ebbf9f8 100644 --- a/web/src/lib/components/admin-page/server-stats/stats-card.svelte +++ b/web/src/lib/components/admin-page/server-stats/stats-card.svelte @@ -7,7 +7,7 @@ $: zeros = () => { let result = ''; - const maxLength = 9; + const maxLength = 13; const valueLength = parseInt(value).toString().length; const zeroLength = maxLength - valueLength; for (let i = 0; i < zeroLength; i++) { @@ -18,7 +18,7 @@
From 6f5d60fb629ab3e9c272b217039628e8658d977f Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Sun, 20 Nov 2022 13:13:27 -0600 Subject: [PATCH 03/30] Up version for release --- mobile/android/fastlane/Fastfile | 4 ++-- mobile/ios/fastlane/Fastfile | 2 +- mobile/pubspec.yaml | 2 +- server/apps/immich/src/constants/server_version.constant.ts | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index ed9e512b54..c696933814 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 54, - "android.injected.version.name" => "1.35.0", + "android.injected.version.code" => 55, + "android.injected.version.name" => "1.36.0", } ) upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index c40114ef93..d9d38e6b33 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Beta" lane :beta do increment_version_number( - version_number: "1.35.0" + version_number: "1.36.0" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 1949ce7145..2d7daa0641 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: "none" -version: 1.35.0+54 +version: 1.36.0+55 environment: sdk: ">=2.17.0 <3.0.0" diff --git a/server/apps/immich/src/constants/server_version.constant.ts b/server/apps/immich/src/constants/server_version.constant.ts index 0131b25f49..b1d5de7fd0 100644 --- a/server/apps/immich/src/constants/server_version.constant.ts +++ b/server/apps/immich/src/constants/server_version.constant.ts @@ -10,7 +10,7 @@ export interface IServerVersion { export const serverVersion: IServerVersion = { major: 1, - minor: 35, + minor: 36, patch: 0, - build: 54, + build: 55, }; From 9d2c30298e3688044d3f96a0b041a3585412b75e Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Sun, 20 Nov 2022 14:11:33 -0600 Subject: [PATCH 04/30] Added changelog for mobile --- .../metadata/android/en-US/changelogs/55.txt | 3 +++ mobile/android/fastlane/report.xml | 6 +++--- mobile/ios/Runner.xcodeproj/project.pbxproj | 6 +++--- mobile/ios/Runner/Info.plist | 4 ++-- mobile/ios/fastlane/report.xml | 12 ++++++------ 5 files changed, 17 insertions(+), 14 deletions(-) create mode 100644 mobile/android/fastlane/metadata/android/en-US/changelogs/55.txt diff --git a/mobile/android/fastlane/metadata/android/en-US/changelogs/55.txt b/mobile/android/fastlane/metadata/android/en-US/changelogs/55.txt new file mode 100644 index 0000000000..ea6099549b --- /dev/null +++ b/mobile/android/fastlane/metadata/android/en-US/changelogs/55.txt @@ -0,0 +1,3 @@ +* Added OAuth login option +* Tidy-up dependencies, remove unused, replace rarely used ones +* Added view LivePhotos feature \ No newline at end of file diff --git a/mobile/android/fastlane/report.xml b/mobile/android/fastlane/report.xml index 35596dd5e1..7e03558dc7 100644 --- a/mobile/android/fastlane/report.xml +++ b/mobile/android/fastlane/report.xml @@ -5,17 +5,17 @@ - + - + - + diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 37e0c55524..450026cde8 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -360,7 +360,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 68; + CURRENT_PROJECT_VERSION = 71; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -495,7 +495,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 68; + CURRENT_PROJECT_VERSION = 71; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -522,7 +522,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 68; + CURRENT_PROJECT_VERSION = 71; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 9e13c18b48..287b93518a 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -17,11 +17,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.34.0 + 1.36.0 CFBundleSignature ???? CFBundleVersion - 68 + 71 LSRequiresIPhoneOS MGLMapboxMetricsEnabledSettingShownInApp diff --git a/mobile/ios/fastlane/report.xml b/mobile/ios/fastlane/report.xml index cccb701b0a..775bde2757 100644 --- a/mobile/ios/fastlane/report.xml +++ b/mobile/ios/fastlane/report.xml @@ -5,32 +5,32 @@ - + - + - + - + - + - + From 37a4f4a39fcc07f7a24d5393f574908e9bb366ab Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Sun, 20 Nov 2022 14:18:53 -0600 Subject: [PATCH 05/30] Update redirect picture in documentation --- docs/docs/usage/img/authentik-redirect.png | Bin 52213 -> 36795 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/docs/usage/img/authentik-redirect.png b/docs/docs/usage/img/authentik-redirect.png index c16c03bab5cb8310a291f7cce5b5a627fad1675c..6cae28bbfe68abc0e2394bd626571fcc652cf5a9 100644 GIT binary patch literal 36795 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�t^WIp{Qvz&a(MP1+B<|$pWl_hz7M=~ MZX0S;sXILVA05r<2><{9 literal 52213 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 From 21fd08e0fb1042c3aa11efee6f6193fcdd9f5ac6 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Sun, 20 Nov 2022 14:42:09 -0600 Subject: [PATCH 06/30] Update Readme --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8c5bca08be..ab12842568 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,9 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM | Search by metadata, objects and image tags | Yes | No | | Administrative functions (user management) | N/A | Yes | | Background backup | Android | N/A | -| Virtual scroll | N/A | Yes | +| Virtual scroll | Yes | Yes | +| OAuth Support | Yes | Yes | +| LivePhotos Backup and Playback (iOS only) | Yes | Yes | # Support the project From 88b8d34aa60e84d3af99ba3be462e3788c97cd81 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Sun, 20 Nov 2022 16:44:33 -0600 Subject: [PATCH 07/30] Update .env.example file --- docker/.env.example | 45 ++++++++++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/docker/.env.example b/docker/.env.example index a594f9c0bf..a2053e29af 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -23,7 +23,9 @@ REDIS_HOSTNAME=immich_redis # REDIS_SOCKET= ################################################################################### -# Upload File Config +# Upload File Location +# +# This is the location where uploaded files are stored. ################################################################################### UPLOAD_LOCATION=absolute_location_on_your_machine_where_you_want_to_store_the_backup @@ -36,19 +38,17 @@ LOG_LEVEL=simple ################################################################################### # JWT SECRET -################################################################################### - +# # This JWT_SECRET is used to sign the authentication keys for user login # You should set it to a long randomly generated value # You can use this command to generate one: openssl rand -base64 128 +################################################################################### + JWT_SECRET= ################################################################################### # Reverse Geocoding -#################################################################################### - -# DISABLE_REVERSE_GEOCODING=false - +# # Reverse geocoding is done locally which has a small impact on memory usage # This memory usage can be altered by changing the REVERSE_GEOCODING_PRECISION variable # This ranges from 0-3 with 3 being the most precise @@ -56,25 +56,44 @@ JWT_SECRET= # 2 - Cities > 1000 population: ~150MB RAM # 1 - Cities > 5000 population: ~80MB RAM # 0 - Cities > 15000 population: ~40MB RAM +#################################################################################### +# DISABLE_REVERSE_GEOCODING=false # REVERSE_GEOCODING_PRECISION=3 #################################################################################### # WEB - Optional -#################################################################################### - +# # Custom message on the login page, should be written in HTML form. -# For example PUBLIC_LOGIN_PAGE_MESSAGE="This is a demo instance of Immich.

Email: demo@demo.de
Password: demo" +# For example: +# PUBLIC_LOGIN_PAGE_MESSAGE="This is a demo instance of Immich.

Email: demo@demo.de
Password: demo" +#################################################################################### PUBLIC_LOGIN_PAGE_MESSAGE= #################################################################################### # Alternative Service Addresses - Optional -#################################################################################### - -# This is an advanced feature for users who may be running their immich services on different hosts. It will not change which address or port that services bind to within their containers, but it will change where other services look for their peers. +# +# This is an advanced feature for users who may be running their immich services on different hosts. +# It will not change which address or port that services bind to within their containers, but it will change where other services look for their peers. # Note: immich-microservices is bound to 3002, but no references are made +#################################################################################### # IMMICH_WEB_URL=http://immich-web:3000 # IMMICH_SERVER_URL=http://immich-server:3001 # IMMICH_MACHINE_LEARNING_URL=http://immich-machine-learning:3003 + +#################################################################################### +# OAuth Setting - Optional +# +# These setting will enable OAuth login for your instance of Immich +# Folow the instructions in the page https://immich.app/docs/usage/oauth to set up your OAuth provider +#################################################################################### + +# OAUTH_ENABLED=false +# OAUTH_ISSUER_URL= +# OAUTH_CLIENT_ID= +# OAUTH_CLIENT_SECRET= +# OAUTH_BUTTON_TEXT=Login with OAuth +# OAUTH_AUTO_REGISTER=true +# OAUTH_SCOPE="openid profile email" From a2f3b2199abd005b59f9f1fc8a1824ef64017c37 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 20 Nov 2022 23:25:03 -0600 Subject: [PATCH 08/30] fix(server): Admin user not created (#996) --- server/apps/immich/src/api-v1/user/user-repository.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/server/apps/immich/src/api-v1/user/user-repository.ts b/server/apps/immich/src/api-v1/user/user-repository.ts index 788b37db0c..e9bccf1e79 100644 --- a/server/apps/immich/src/api-v1/user/user-repository.ts +++ b/server/apps/immich/src/api-v1/user/user-repository.ts @@ -59,7 +59,6 @@ export class UserRepository implements IUserRepository { user.salt = await bcrypt.genSalt(); user.password = await this.hashPassword(user.password, user.salt); } - user.isAdmin = false; return this.userRepository.save(user); } From 56ce747ffc61723aac2730ba3f9af54d601cd734 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 21 Nov 2022 05:29:43 -0600 Subject: [PATCH 09/30] fix(mobile): freeze on splash screen due to accessing bad state (#998) --- .../providers/authentication.provider.dart | 11 +++--- mobile/lib/modules/login/ui/login_form.dart | 1 + mobile/lib/shared/views/splash_screen.dart | 34 ++++++++++-------- .../openapi/lib/model/asset_response_dto.dart | Bin 9013 -> 9208 bytes .../openapi/lib/model/user_response_dto.dart | Bin 5587 -> 5742 bytes 5 files changed, 28 insertions(+), 18 deletions(-) diff --git a/mobile/lib/modules/login/providers/authentication.provider.dart b/mobile/lib/modules/login/providers/authentication.provider.dart index 89202f838a..1864d0e024 100644 --- a/mobile/lib/modules/login/providers/authentication.provider.dart +++ b/mobile/lib/modules/login/providers/authentication.provider.dart @@ -90,6 +90,7 @@ class AuthenticationNotifier extends StateNotifier { return setSuccessLoginInfo( accessToken: loginResponse.accessToken, + serverUrl: serverEndpoint, isSavedLoginInfo: isSavedLoginInfo, ); } catch (e) { @@ -159,16 +160,18 @@ class AuthenticationNotifier extends StateNotifier { Future setSuccessLoginInfo({ required String accessToken, + required String serverUrl, required bool isSavedLoginInfo, }) async { - Hive.box(userInfoBox).put(accessTokenKey, accessToken); - _apiService.setAccessToken(accessToken); var userResponseDto = await _apiService.userApi.getMyUserInfo(); if (userResponseDto != null) { + var userInfoHiveBox = await Hive.openBox(userInfoBox); var deviceInfo = await _deviceInfoService.getDeviceInfo(); - Hive.box(userInfoBox).put(deviceIdKey, deviceInfo["deviceId"]); + userInfoHiveBox.put(deviceIdKey, deviceInfo["deviceId"]); + userInfoHiveBox.put(accessTokenKey, accessToken); + userInfoHiveBox.put(serverEndpointKey, serverUrl); state = state.copyWith( isAuthenticated: true, @@ -191,7 +194,7 @@ class AuthenticationNotifier extends StateNotifier { email: "", password: "", isSaveLogin: true, - serverUrl: Hive.box(userInfoBox).get(serverEndpointKey), + serverUrl: serverUrl, accessToken: accessToken, ), ); diff --git a/mobile/lib/modules/login/ui/login_form.dart b/mobile/lib/modules/login/ui/login_form.dart index 82f723f01e..a1c32f80d7 100644 --- a/mobile/lib/modules/login/ui/login_form.dart +++ b/mobile/lib/modules/login/ui/login_form.dart @@ -380,6 +380,7 @@ class OAuthLoginButton extends ConsumerWidget { .setSuccessLoginInfo( accessToken: loginResponseDto.accessToken, isSavedLoginInfo: isSavedLoginInfo, + serverUrl: serverEndpointController.text, ); if (isSuccess) { diff --git a/mobile/lib/shared/views/splash_screen.dart b/mobile/lib/shared/views/splash_screen.dart index b62e5d6b09..e659c1241a 100644 --- a/mobile/lib/shared/views/splash_screen.dart +++ b/mobile/lib/shared/views/splash_screen.dart @@ -20,22 +20,28 @@ class SplashScreenPage extends HookConsumerWidget { Hive.box(hiveLoginInfoBox).get(savedLoginInfoKey); void performLoggingIn() async { - if (loginInfo != null) { - // Make sure API service is initialized - apiService.setEndpoint(loginInfo.serverUrl); + try { + if (loginInfo != null) { + // Make sure API service is initialized + apiService.setEndpoint(loginInfo.serverUrl); - var isSuccess = - await ref.read(authenticationProvider.notifier).setSuccessLoginInfo( - accessToken: loginInfo.accessToken, - isSavedLoginInfo: true, - ); - if (isSuccess) { - // Resume backup (if enable) then navigate - ref.watch(backupProvider.notifier).resumeBackup(); - AutoRouter.of(context).replace(const TabControllerRoute()); - } else { - AutoRouter.of(context).replace(const LoginRoute()); + var isSuccess = await ref + .read(authenticationProvider.notifier) + .setSuccessLoginInfo( + accessToken: loginInfo.accessToken, + isSavedLoginInfo: true, + serverUrl: loginInfo.serverUrl, + ); + if (isSuccess) { + // Resume backup (if enable) then navigate + ref.watch(backupProvider.notifier).resumeBackup(); + AutoRouter.of(context).replace(const TabControllerRoute()); + } else { + AutoRouter.of(context).replace(const LoginRoute()); + } } + } catch (_) { + AutoRouter.of(context).replace(const LoginRoute()); } } diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart index 164a426dc103d23997d17babf675888cac06d07e..f916088ba26543b3b67794c6c50546debace41d1 100644 GIT binary patch delta 528 zcmdn$_QQR{U3NAu1rV5gmt6|R{LZdCS(HOzvH^$8WOoju$t4_Wlc#cMOy0p^F!?cu z&SWl5t;zbFI+MeIxRz66@&X_}48+ekEhbBHX-u}^(wdye6)|}`m*(VqToNJ*3L2Ry zdKrnu8P54BshSGfKr1J+aVtv#CGyMjQj0t>WbL@+CP#5AOs?Rzn7oc#cJfJXIT?^K z#cqjZ`9+x}sp!W2=Qf*c%p)_|m&assJ&()egFyN-kJ)5(-Xy5EHh1zqW}KYIFD)FO zRh*v}U8J6wq8_VYtB{#u%{6%ezv1M|{ECym@heZ37FaR)sDSF^Hv;ODMFr<<-YmG2 zMO@1GA}VVGg$%T5FMy#llc_Hn1QmJYh;?4@hf8iDyyCRURHMUb$Rj0kqS5L qpmu>`GBvNHs8X*iF{d0DJRuR0meeeZVps@%m@H~*q0js delta 437 zcmZutO-mbL5GE+N*$>?Wlhj1(*d^Ir6H-V^50;=bReylq#I~#Z;;tlF*^jotc*qEFCJ77FRxOx3liUgv5ml|-xvHS7ozIO38kp^RKoM$dVXW{}zB^|R;RyAC^>R5r(@N8yMS%fEP*p^1HFXiz|dWdW3 z88o?o8F>`1IJ$BT2XY?2<=SWE1zsvOyjPxLPk9p$4_+n;M6cHauf62!VP}Ks%T%Wx zc`g!RP%S`LA7fF~5UE9cQFk$u(!o+DKBm6KM*3%e$_fJJhlXL&Chy^fn`{YM&cUag z!^17JH|xLoER6g%g?@CqfG6Ye2PgeQcBgH%+*RsbQlU+Jr)-=V`_Ol9;hdBN@%Q_k f>RYQBu?nsE-aBih$)@gD1;=BP@${n~iG=tMoU4kE diff --git a/mobile/openapi/lib/model/user_response_dto.dart b/mobile/openapi/lib/model/user_response_dto.dart index a6242d2c8411dc4e66c27c2fa6043c553fb2308c..63c176378fe413d055c07013c9612d29f6868e7a 100644 GIT binary patch delta 503 zcmcbt{Z3~?4Ko{;0tig5VU~h1r!gzZfCckQGE$55GE)?6Z54o|ni^E?DQ2a~&zZF* z^RQ@4)@RY09KvETxt+yf@*x(r$&XpQCTp{5P7Y!f6IW2sNX<>m%+bq8EY5JwPf68O z&;}YYxtdje@(fm)$(vYZC!b|inf#trLlUSqv)C~uH!}}iwGLYt)McBS*bEpaZ($do ze3o5y@*8&5$-*4UlPx$@C&zObPoBbIz4;=?9wu>peTBs0;?$xN4Gm3&YKWT^^z|pt z<5pt=Qir)SK$I3wh9*#@f nZbmyK2c+he6jkb#CFYcZ%>}y&NWkp0g0n!jLcr#5k!nT&Zy|`{ delta 360 zcmZutyGp}Q043&OjMdtQg;u1um9_~DMG+A!oy1vi6I{~f+ClGIY0;@}sQ^9SMw z2$@6?+}s>QoCG%~-K7r32Tq>lJg4)f-dE29Hb+dI+P>M+EZH|qPbH3TP*)*AfK#@Z z{B|`{Cyhq5B;~S%XSRq*u7nkC3A+)WaI3iF>KJnc6!^H7mT(;LnYe~W zu@NSxt|t<0#gS>` Date: Mon, 21 Nov 2022 05:41:44 -0600 Subject: [PATCH 10/30] Added hotfix release note --- mobile/android/fastlane/Fastfile | 4 ++-- .../metadata/android/en-US/changelogs/56.txt | 2 ++ mobile/android/fastlane/report.xml | 6 +++--- mobile/ios/Runner.xcodeproj/project.pbxproj | 6 +++--- mobile/ios/Runner/Info.plist | 4 ++-- mobile/ios/fastlane/Fastfile | 2 +- mobile/ios/fastlane/report.xml | 12 ++++++------ mobile/pubspec.yaml | 2 +- .../immich/src/constants/server_version.constant.ts | 4 ++-- 9 files changed, 22 insertions(+), 20 deletions(-) create mode 100644 mobile/android/fastlane/metadata/android/en-US/changelogs/56.txt diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index c696933814..e345b9a121 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 55, - "android.injected.version.name" => "1.36.0", + "android.injected.version.code" => 56, + "android.injected.version.name" => "1.36.1", } ) upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') diff --git a/mobile/android/fastlane/metadata/android/en-US/changelogs/56.txt b/mobile/android/fastlane/metadata/android/en-US/changelogs/56.txt new file mode 100644 index 0000000000..f90fc5722b --- /dev/null +++ b/mobile/android/fastlane/metadata/android/en-US/changelogs/56.txt @@ -0,0 +1,2 @@ +* Fixed freezed splash screen +* Fixed OIDC redirect but not logging in \ No newline at end of file diff --git a/mobile/android/fastlane/report.xml b/mobile/android/fastlane/report.xml index 7e03558dc7..da68f11b20 100644 --- a/mobile/android/fastlane/report.xml +++ b/mobile/android/fastlane/report.xml @@ -5,17 +5,17 @@ - + - + - + diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 450026cde8..d546a2daed 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -360,7 +360,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 72; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -495,7 +495,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 72; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -522,7 +522,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 71; + CURRENT_PROJECT_VERSION = 72; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 287b93518a..cacb14f32a 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -17,11 +17,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.36.0 + 1.36.1 CFBundleSignature ???? CFBundleVersion - 71 + 72 LSRequiresIPhoneOS MGLMapboxMetricsEnabledSettingShownInApp diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index d9d38e6b33..7dc179e228 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Beta" lane :beta do increment_version_number( - version_number: "1.36.0" + version_number: "1.36.1" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/ios/fastlane/report.xml b/mobile/ios/fastlane/report.xml index 775bde2757..aadffa85f3 100644 --- a/mobile/ios/fastlane/report.xml +++ b/mobile/ios/fastlane/report.xml @@ -5,32 +5,32 @@ - + - + - + - + - + - + diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 2d7daa0641..b76702f0fc 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: "none" -version: 1.36.0+55 +version: 1.36.1+56 environment: sdk: ">=2.17.0 <3.0.0" diff --git a/server/apps/immich/src/constants/server_version.constant.ts b/server/apps/immich/src/constants/server_version.constant.ts index b1d5de7fd0..6febc09400 100644 --- a/server/apps/immich/src/constants/server_version.constant.ts +++ b/server/apps/immich/src/constants/server_version.constant.ts @@ -11,6 +11,6 @@ export interface IServerVersion { export const serverVersion: IServerVersion = { major: 1, minor: 36, - patch: 0, - build: 55, + patch: 1, + build: 56, }; From 39b7ab66d4ef839a7d306724400c6b1f93029b9e Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 21 Nov 2022 06:13:14 -0600 Subject: [PATCH 11/30] chore(mobile): clean up linter problems (#1000) --- .../modules/album/services/album.service.dart | 5 ++- .../modules/album/ui/album_viewer_appbar.dart | 24 +++++----- .../album/ui/album_viewer_thumbnail.dart | 30 ++++++------- .../modules/album/ui/month_group_title.dart | 10 ++--- .../album/ui/selection_thumbnail_image.dart | 4 +- .../album/views/album_viewer_page.dart | 35 ++++++++------- .../album/views/asset_selection_page.dart | 8 ++-- .../album/views/create_album_page.dart | 40 ++++++++--------- ...lect_additional_user_for_sharing_page.dart | 12 ++--- .../views/select_user_for_sharing_page.dart | 13 +++--- .../lib/modules/album/views/sharing_page.dart | 8 ++-- .../image_viewer_page_state.provider.dart | 4 +- .../asset_viewer/ui/exif_bottom_sheet.dart | 8 ++-- .../background_service/localization.dart | 2 + .../modules/backup/ui/album_info_card.dart | 8 ++-- .../backup/views/album_preview_page.dart | 8 ++-- .../views/backup_album_selection_page.dart | 12 ++--- .../backup/views/backup_controller_page.dart | 44 +++++++++---------- .../modules/home/services/asset.service.dart | 2 +- .../ui/profile_drawer/profile_drawer.dart | 8 ++-- .../profile_drawer/profile_drawer_header.dart | 10 ++--- .../ui/profile_drawer/server_info_box.dart | 4 +- .../login/providers/oauth.provider.dart | 2 +- mobile/lib/modules/login/ui/login_form.dart | 2 +- .../lib/modules/search/views/search_page.dart | 14 +++--- .../search/views/search_result_page.dart | 16 +++---- .../modules/settings/views/settings_page.dart | 1 - .../lib/shared/providers/asset.provider.dart | 7 ++- .../providers/release_info.provider.dart | 9 ++-- mobile/lib/shared/services/cache.service.dart | 1 + mobile/lib/shared/ui/immich_toast.dart | 8 ++-- .../utils/immich_cache_info_repository.dart | 2 + 32 files changed, 188 insertions(+), 173 deletions(-) diff --git a/mobile/lib/modules/album/services/album.service.dart b/mobile/lib/modules/album/services/album.service.dart index 5e53399e35..fd82cd06fc 100644 --- a/mobile/lib/modules/album/services/album.service.dart +++ b/mobile/lib/modules/album/services/album.service.dart @@ -69,7 +69,10 @@ class AlbumService { Iterable assets, ) async { return createAlbum( - _getNextAlbumName(await getAlbums(isShared: false)), assets, []); + _getNextAlbumName(await getAlbums(isShared: false)), + assets, + [], + ); } Future getAlbumDetail(String albumId) async { diff --git a/mobile/lib/modules/album/ui/album_viewer_appbar.dart b/mobile/lib/modules/album/ui/album_viewer_appbar.dart index ccc23ca72f..f49082559a 100644 --- a/mobile/lib/modules/album/ui/album_viewer_appbar.dart +++ b/mobile/lib/modules/album/ui/album_viewer_appbar.dart @@ -34,7 +34,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget { final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText; final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum; - void _onDeleteAlbumPressed(String albumId) async { + void onDeleteAlbumPressed(String albumId) async { ImmichLoadingOverlayController.appLoader.show(); bool isSuccess = @@ -62,7 +62,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget { ImmichLoadingOverlayController.appLoader.hide(); } - void _onLeaveAlbumPressed(String albumId) async { + void onLeaveAlbumPressed(String albumId) async { ImmichLoadingOverlayController.appLoader.show(); bool isSuccess = @@ -84,7 +84,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget { ImmichLoadingOverlayController.appLoader.hide(); } - void _onRemoveFromAlbumPressed(String albumId) async { + void onRemoveFromAlbumPressed(String albumId) async { ImmichLoadingOverlayController.appLoader.show(); bool isSuccess = @@ -110,7 +110,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget { ImmichLoadingOverlayController.appLoader.hide(); } - _buildBottomSheetActionButton() { + buildBottomSheetActionButton() { if (isMultiSelectionEnable) { if (albumInfo.ownerId == userId) { return ListTile( @@ -119,7 +119,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget { 'album_viewer_appbar_share_remove', style: TextStyle(fontWeight: FontWeight.bold), ).tr(), - onTap: () => _onRemoveFromAlbumPressed(albumId), + onTap: () => onRemoveFromAlbumPressed(albumId), ); } else { return const SizedBox(); @@ -132,7 +132,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget { 'album_viewer_appbar_share_delete', style: TextStyle(fontWeight: FontWeight.bold), ).tr(), - onTap: () => _onDeleteAlbumPressed(albumId), + onTap: () => onDeleteAlbumPressed(albumId), ); } else { return ListTile( @@ -141,13 +141,13 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget { 'album_viewer_appbar_share_leave', style: TextStyle(fontWeight: FontWeight.bold), ).tr(), - onTap: () => _onLeaveAlbumPressed(albumId), + onTap: () => onLeaveAlbumPressed(albumId), ); } } } - void _buildBottomSheet() { + void buildBottomSheet() { showModalBottomSheet( backgroundColor: Theme.of(context).scaffoldBackgroundColor, isScrollControlled: false, @@ -157,7 +157,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - _buildBottomSheetActionButton(), + buildBottomSheetActionButton(), ], ), ); @@ -165,7 +165,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget { ); } - _buildLeadingButton() { + buildLeadingButton() { if (isMultiSelectionEnable) { return IconButton( onPressed: () => ref @@ -204,7 +204,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget { return AppBar( elevation: 0, - leading: _buildLeadingButton(), + leading: buildLeadingButton(), title: isMultiSelectionEnable ? Text('${selectedAssetsInAlbum.length}') : null, @@ -212,7 +212,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget { actions: [ IconButton( splashRadius: 25, - onPressed: _buildBottomSheet, + onPressed: buildBottomSheet, icon: const Icon(Icons.more_horiz_rounded), ), ], diff --git a/mobile/lib/modules/album/ui/album_viewer_thumbnail.dart b/mobile/lib/modules/album/ui/album_viewer_thumbnail.dart index 6566e76434..e34060def9 100644 --- a/mobile/lib/modules/album/ui/album_viewer_thumbnail.dart +++ b/mobile/lib/modules/album/ui/album_viewer_thumbnail.dart @@ -27,7 +27,7 @@ class AlbumViewerThumbnail extends HookConsumerWidget { final isMultiSelectionEnable = ref.watch(assetSelectionProvider).isMultiselectEnable; - _viewAsset() { + viewAsset() { AutoRouter.of(context).push( GalleryViewerRoute( asset: asset, @@ -47,18 +47,18 @@ class AlbumViewerThumbnail extends HookConsumerWidget { } } - _enableMultiSelection() { + enableMultiSelection() { ref.watch(assetSelectionProvider.notifier).enableMultiselection(); ref .watch(assetSelectionProvider.notifier) .addAssetsInAlbumViewer([asset]); } - _disableMultiSelection() { + disableMultiSelection() { ref.watch(assetSelectionProvider.notifier).disableMultiselection(); } - _buildVideoLabel() { + buildVideoLabel() { return Positioned( top: 5, right: 5, @@ -80,7 +80,7 @@ class AlbumViewerThumbnail extends HookConsumerWidget { ); } - _buildAssetStoreLocationIcon() { + buildAssetStoreLocationIcon() { return Positioned( right: 10, bottom: 5, @@ -94,7 +94,7 @@ class AlbumViewerThumbnail extends HookConsumerWidget { ); } - _buildAssetSelectionIcon() { + buildAssetSelectionIcon() { bool isSelected = selectedAssetsInAlbumViewer.contains(asset); return Positioned( @@ -112,21 +112,21 @@ class AlbumViewerThumbnail extends HookConsumerWidget { ); } - _buildThumbnailImage() { + buildThumbnailImage() { return Container( decoration: BoxDecoration(border: drawBorderColor()), child: ImmichImage(asset, width: 300, height: 300), ); } - _handleSelectionGesture() { + handleSelectionGesture() { if (selectedAssetsInAlbumViewer.contains(asset)) { ref .watch(assetSelectionProvider.notifier) .removeAssetsInAlbumViewer([asset]); if (selectedAssetsInAlbumViewer.isEmpty) { - _disableMultiSelection(); + disableMultiSelection(); } } else { ref @@ -136,14 +136,14 @@ class AlbumViewerThumbnail extends HookConsumerWidget { } return GestureDetector( - onTap: isMultiSelectionEnable ? _handleSelectionGesture : _viewAsset, - onLongPress: _enableMultiSelection, + onTap: isMultiSelectionEnable ? handleSelectionGesture : viewAsset, + onLongPress: enableMultiSelection, child: Stack( children: [ - _buildThumbnailImage(), - if (showStorageIndicator) _buildAssetStoreLocationIcon(), - if (!asset.isImage) _buildVideoLabel(), - if (isMultiSelectionEnable) _buildAssetSelectionIcon(), + buildThumbnailImage(), + if (showStorageIndicator) buildAssetStoreLocationIcon(), + if (!asset.isImage) buildVideoLabel(), + if (isMultiSelectionEnable) buildAssetSelectionIcon(), ], ), ); diff --git a/mobile/lib/modules/album/ui/month_group_title.dart b/mobile/lib/modules/album/ui/month_group_title.dart index e3a772d287..5d33d44ab0 100644 --- a/mobile/lib/modules/album/ui/month_group_title.dart +++ b/mobile/lib/modules/album/ui/month_group_title.dart @@ -21,7 +21,7 @@ class MonthGroupTitle extends HookConsumerWidget { ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum; final isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist; - _handleTitleIconClick() { + handleTitleIconClick() { HapticFeedback.heavyImpact(); if (isAlbumExist) { @@ -61,7 +61,7 @@ class MonthGroupTitle extends HookConsumerWidget { } } - _getSimplifiedMonth() { + getSimplifiedMonth() { var monthAndYear = month.split(','); var yearText = monthAndYear[1].trim(); var monthText = monthAndYear[0].trim(); @@ -85,7 +85,7 @@ class MonthGroupTitle extends HookConsumerWidget { child: Row( children: [ GestureDetector( - onTap: _handleTitleIconClick, + onTap: handleTitleIconClick, child: selectedDateGroup.contains(month) ? Icon( Icons.check_circle_rounded, @@ -97,11 +97,11 @@ class MonthGroupTitle extends HookConsumerWidget { ), ), GestureDetector( - onTap: _handleTitleIconClick, + onTap: handleTitleIconClick, child: Padding( padding: const EdgeInsets.only(left: 8.0), child: Text( - _getSimplifiedMonth(), + getSimplifiedMonth(), style: TextStyle( fontSize: 24, color: Theme.of(context).primaryColor, diff --git a/mobile/lib/modules/album/ui/selection_thumbnail_image.dart b/mobile/lib/modules/album/ui/selection_thumbnail_image.dart index 09b3160b62..25d8c01de0 100644 --- a/mobile/lib/modules/album/ui/selection_thumbnail_image.dart +++ b/mobile/lib/modules/album/ui/selection_thumbnail_image.dart @@ -18,7 +18,7 @@ class SelectionThumbnailImage extends HookConsumerWidget { ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum; var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist; - Widget _buildSelectionIcon(Asset asset) { + Widget buildSelectionIcon(Asset asset) { var isSelected = selectedAsset.map((item) => item.id).contains(asset.id); var isNewlySelected = newAssetsForAlbum.map((item) => item.id).contains(asset.id); @@ -111,7 +111,7 @@ class SelectionThumbnailImage extends HookConsumerWidget { padding: const EdgeInsets.all(3.0), child: Align( alignment: Alignment.topLeft, - child: _buildSelectionIcon(asset), + child: buildSelectionIcon(asset), ), ), if (!asset.isImage) diff --git a/mobile/lib/modules/album/views/album_viewer_page.dart b/mobile/lib/modules/album/views/album_viewer_page.dart index 9e9c4af41a..65db82eb6b 100644 --- a/mobile/lib/modules/album/views/album_viewer_page.dart +++ b/mobile/lib/modules/album/views/album_viewer_page.dart @@ -37,7 +37,7 @@ class AlbumViewerPage extends HookConsumerWidget { /// Find out if the assets in album exist on the device /// If they exist, add to selected asset state to show they are already selected. - void _onAddPhotosPressed(AlbumResponseDto albumInfo) async { + void onAddPhotosPressed(AlbumResponseDto albumInfo) async { if (albumInfo.assets.isNotEmpty == true) { ref.watch(assetSelectionProvider.notifier).addNewAssets( albumInfo.assets.map((e) => Asset.remote(e)).toList(), @@ -60,7 +60,8 @@ class AlbumViewerPage extends HookConsumerWidget { albumId, ); - if (addAssetsResult != null && addAssetsResult.successfullyAdded > 0) { + if (addAssetsResult != null && + addAssetsResult.successfullyAdded > 0) { ref.refresh(sharedAlbumDetailProvider(albumId)); } @@ -73,7 +74,7 @@ class AlbumViewerPage extends HookConsumerWidget { } } - void _onAddUsersPressed(AlbumResponseDto albumInfo) async { + void onAddUsersPressed(AlbumResponseDto albumInfo) async { List? sharedUserIds = await AutoRouter.of(context).push?>( SelectAdditionalUserForSharingRoute(albumInfo: albumInfo), @@ -94,7 +95,7 @@ class AlbumViewerPage extends HookConsumerWidget { } } - Widget _buildTitle(AlbumResponseDto albumInfo) { + Widget buildTitle(AlbumResponseDto albumInfo) { return Padding( padding: const EdgeInsets.only(left: 8, right: 8, top: 16), child: userId == albumInfo.ownerId @@ -115,7 +116,7 @@ class AlbumViewerPage extends HookConsumerWidget { ); } - Widget _buildAlbumDateRange(AlbumResponseDto albumInfo) { + Widget buildAlbumDateRange(AlbumResponseDto albumInfo) { String startDate = ""; DateTime parsedStartDate = DateTime.parse(albumInfo.assets.first.createdAt); @@ -148,14 +149,14 @@ class AlbumViewerPage extends HookConsumerWidget { ); } - Widget _buildHeader(AlbumResponseDto albumInfo) { + Widget buildHeader(AlbumResponseDto albumInfo) { return SliverToBoxAdapter( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildTitle(albumInfo), + buildTitle(albumInfo), if (albumInfo.assets.isNotEmpty == true) - _buildAlbumDateRange(albumInfo), + buildAlbumDateRange(albumInfo), if (albumInfo.shared) SizedBox( height: 60, @@ -188,7 +189,7 @@ class AlbumViewerPage extends HookConsumerWidget { ); } - Widget _buildImageGrid(AlbumResponseDto albumInfo) { + Widget buildImageGrid(AlbumResponseDto albumInfo) { final appSettingService = ref.watch(appSettingsServiceProvider); final bool showStorageIndicator = appSettingService.getSetting(AppSettingsEnum.storageIndicator); @@ -220,7 +221,7 @@ class AlbumViewerPage extends HookConsumerWidget { return const SliverToBoxAdapter(); } - Widget _buildControlButton(AlbumResponseDto albumInfo) { + Widget buildControlButton(AlbumResponseDto albumInfo) { return Padding( padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 8), child: SizedBox( @@ -230,13 +231,13 @@ class AlbumViewerPage extends HookConsumerWidget { children: [ AlbumActionOutlinedButton( iconData: Icons.add_photo_alternate_outlined, - onPressed: () => _onAddPhotosPressed(albumInfo), + onPressed: () => onAddPhotosPressed(albumInfo), labelText: "share_add_photos".tr(), ), if (userId == albumInfo.ownerId) AlbumActionOutlinedButton( iconData: Icons.person_add_alt_rounded, - onPressed: () => _onAddUsersPressed(albumInfo), + onPressed: () => onAddUsersPressed(albumInfo), labelText: "album_viewer_page_share_add_users".tr(), ), ], @@ -245,7 +246,7 @@ class AlbumViewerPage extends HookConsumerWidget { ); } - Widget _buildBody(AlbumResponseDto albumInfo) { + Widget buildBody(AlbumResponseDto albumInfo) { return GestureDetector( onTap: () { titleFocusNode.unfocus(); @@ -257,7 +258,7 @@ class AlbumViewerPage extends HookConsumerWidget { child: CustomScrollView( controller: scrollController, slivers: [ - _buildHeader(albumInfo), + buildHeader(albumInfo), SliverPersistentHeader( pinned: true, delegate: ImmichSliverPersistentAppBarDelegate( @@ -265,11 +266,11 @@ class AlbumViewerPage extends HookConsumerWidget { maxHeight: 50, child: Container( color: Theme.of(context).scaffoldBackgroundColor, - child: _buildControlButton(albumInfo), + child: buildControlButton(albumInfo), ), ), ), - _buildImageGrid(albumInfo) + buildImageGrid(albumInfo) ], ), ), @@ -293,7 +294,7 @@ class AlbumViewerPage extends HookConsumerWidget { ), body: albumInfo.when( data: (albumInfo) => albumInfo != null - ? _buildBody(albumInfo) + ? buildBody(albumInfo) : const Center( child: CircularProgressIndicator(), ), diff --git a/mobile/lib/modules/album/views/asset_selection_page.dart b/mobile/lib/modules/album/views/asset_selection_page.dart index 226d6b0285..3654545179 100644 --- a/mobile/lib/modules/album/views/asset_selection_page.dart +++ b/mobile/lib/modules/album/views/asset_selection_page.dart @@ -25,7 +25,7 @@ class AssetSelectionPage extends HookConsumerWidget { List imageGridGroup = []; - String _buildAssetCountText() { + String buildAssetCountText() { if (isAlbumExist) { return (selectedAssets.length + newAssetsForAlbum.length).toString(); } else { @@ -33,7 +33,7 @@ class AssetSelectionPage extends HookConsumerWidget { } } - Widget _buildBody() { + Widget buildBody() { assetGroupMonthYear.forEach((monthYear, assetGroup) { imageGridGroup .add(MonthGroupTitle(month: monthYear, assetGroup: assetGroup)); @@ -71,7 +71,7 @@ class AssetSelectionPage extends HookConsumerWidget { style: TextStyle(fontSize: 18), ).tr() : Text( - _buildAssetCountText(), + buildAssetCountText(), style: const TextStyle(fontSize: 18), ), centerTitle: false, @@ -94,7 +94,7 @@ class AssetSelectionPage extends HookConsumerWidget { ), ], ), - body: _buildBody(), + body: buildBody(), ); } } diff --git a/mobile/lib/modules/album/views/create_album_page.dart b/mobile/lib/modules/album/views/create_album_page.dart index 374d8fb12f..18d3d978a8 100644 --- a/mobile/lib/modules/album/views/create_album_page.dart +++ b/mobile/lib/modules/album/views/create_album_page.dart @@ -29,11 +29,11 @@ class CreateAlbumPage extends HookConsumerWidget { ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum; final isDarkTheme = Theme.of(context).brightness == Brightness.dark; - _showSelectUserPage() { + showSelectUserPage() { AutoRouter.of(context).push(const SelectUserForSharingRoute()); } - void _onBackgroundTapped() { + void onBackgroundTapped() { albumTitleTextFieldFocusNode.unfocus(); isAlbumTitleTextFieldFocus.value = false; @@ -45,7 +45,7 @@ class CreateAlbumPage extends HookConsumerWidget { } } - _onSelectPhotosButtonPressed() async { + onSelectPhotosButtonPressed() async { ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(false); AssetSelectionPageResult? selectedAsset = await AutoRouter.of(context) @@ -56,7 +56,7 @@ class CreateAlbumPage extends HookConsumerWidget { } } - _buildTitleInputField() { + buildTitleInputField() { return Padding( padding: const EdgeInsets.only( right: 10, @@ -71,7 +71,7 @@ class CreateAlbumPage extends HookConsumerWidget { ); } - _buildTitle() { + buildTitle() { if (selectedAssets.isEmpty) { return SliverToBoxAdapter( child: Padding( @@ -90,7 +90,7 @@ class CreateAlbumPage extends HookConsumerWidget { return const SliverToBoxAdapter(); } - _buildSelectPhotosButton() { + buildSelectPhotosButton() { if (selectedAssets.isEmpty) { return SliverToBoxAdapter( child: Padding( @@ -109,7 +109,7 @@ class CreateAlbumPage extends HookConsumerWidget { borderRadius: BorderRadius.circular(5), ), ), - onPressed: _onSelectPhotosButtonPressed, + onPressed: onSelectPhotosButtonPressed, icon: Icon( Icons.add_rounded, color: Theme.of(context).primaryColor, @@ -132,7 +132,7 @@ class CreateAlbumPage extends HookConsumerWidget { return const SliverToBoxAdapter(); } - _buildControlButton() { + buildControlButton() { return Padding( padding: const EdgeInsets.only(left: 12.0, top: 16, bottom: 16), child: SizedBox( @@ -142,7 +142,7 @@ class CreateAlbumPage extends HookConsumerWidget { children: [ AlbumActionOutlinedButton( iconData: Icons.add_photo_alternate_outlined, - onPressed: _onSelectPhotosButtonPressed, + onPressed: onSelectPhotosButtonPressed, labelText: "share_add_photos".tr(), ), ], @@ -151,7 +151,7 @@ class CreateAlbumPage extends HookConsumerWidget { ); } - _buildSelectedImageGrid() { + buildSelectedImageGrid() { if (selectedAssets.isNotEmpty) { return SliverPadding( padding: const EdgeInsets.only(top: 16), @@ -164,7 +164,7 @@ class CreateAlbumPage extends HookConsumerWidget { delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return GestureDetector( - onTap: _onBackgroundTapped, + onTap: onBackgroundTapped, child: SharedAlbumThumbnailImage( asset: selectedAssets.elementAt(index), ), @@ -179,7 +179,7 @@ class CreateAlbumPage extends HookConsumerWidget { return const SliverToBoxAdapter(); } - _createNonSharedAlbum() async { + createNonSharedAlbum() async { var newAlbum = await ref.watch(albumProvider.notifier).createAlbum( ref.watch(albumTitleProvider), ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum, @@ -216,7 +216,7 @@ class CreateAlbumPage extends HookConsumerWidget { if (isSharedAlbum) TextButton( onPressed: albumTitleController.text.isNotEmpty - ? _showSelectUserPage + ? showSelectUserPage : null, child: Text( 'create_shared_album_page_share'.tr(), @@ -230,7 +230,7 @@ class CreateAlbumPage extends HookConsumerWidget { TextButton( onPressed: albumTitleController.text.isNotEmpty && selectedAssets.isNotEmpty - ? _createNonSharedAlbum + ? createNonSharedAlbum : null, child: Text( 'create_shared_album_page_create'.tr(), @@ -242,7 +242,7 @@ class CreateAlbumPage extends HookConsumerWidget { ], ), body: GestureDetector( - onTap: _onBackgroundTapped, + onTap: onBackgroundTapped, child: CustomScrollView( slivers: [ SliverAppBar( @@ -255,15 +255,15 @@ class CreateAlbumPage extends HookConsumerWidget { preferredSize: const Size.fromHeight(66.0), child: Column( children: [ - _buildTitleInputField(), - if (selectedAssets.isNotEmpty) _buildControlButton(), + buildTitleInputField(), + if (selectedAssets.isNotEmpty) buildControlButton(), ], ), ), ), - _buildTitle(), - _buildSelectPhotosButton(), - _buildSelectedImageGrid(), + buildTitle(), + buildSelectPhotosButton(), + buildSelectedImageGrid(), ], ), ), diff --git a/mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart b/mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart index 61ee9a6e6a..553a4daa87 100644 --- a/mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart +++ b/mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart @@ -19,12 +19,12 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget { ref.watch(suggestedSharedUsersProvider); final sharedUsersList = useState>({}); - _addNewUsersHandler() { + addNewUsersHandler() { AutoRouter.of(context) .pop(sharedUsersList.value.map((e) => e.id).toList()); } - _buildTileIcon(UserResponseDto user) { + buildTileIcon(UserResponseDto user) { if (sharedUsersList.value.contains(user)) { return CircleAvatar( backgroundColor: Theme.of(context).primaryColor, @@ -42,7 +42,7 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget { } } - _buildUserList(List users) { + buildUserList(List users) { List usersChip = []; for (var user in sharedUsersList.value) { @@ -84,7 +84,7 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget { shrinkWrap: true, itemBuilder: ((context, index) { return ListTile( - leading: _buildTileIcon(users[index]), + leading: buildTileIcon(users[index]), title: Text( users[index].email, style: const TextStyle( @@ -131,7 +131,7 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget { actions: [ TextButton( onPressed: - sharedUsersList.value.isEmpty ? null : _addNewUsersHandler, + sharedUsersList.value.isEmpty ? null : addNewUsersHandler, child: const Text( "share_add", style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), @@ -147,7 +147,7 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget { ); } - return _buildUserList(users); + return buildUserList(users); }, error: (e, _) => Text("Error loading suggested users $e"), loading: () => const Center( diff --git a/mobile/lib/modules/album/views/select_user_for_sharing_page.dart b/mobile/lib/modules/album/views/select_user_for_sharing_page.dart index 19f007b35b..cbccbb1b3d 100644 --- a/mobile/lib/modules/album/views/select_user_for_sharing_page.dart +++ b/mobile/lib/modules/album/views/select_user_for_sharing_page.dart @@ -20,7 +20,7 @@ class SelectUserForSharingPage extends HookConsumerWidget { AsyncValue> suggestedShareUsers = ref.watch(suggestedSharedUsersProvider); - _createSharedAlbum() async { + createSharedAlbum() async { var newAlbum = await ref.watch(sharedAlbumProvider.notifier).createSharedAlbum( ref.watch(albumTitleProvider), @@ -44,7 +44,7 @@ class SelectUserForSharingPage extends HookConsumerWidget { ); } - _buildTileIcon(UserResponseDto user) { + buildTileIcon(UserResponseDto user) { if (sharedUsersList.value.contains(user)) { return CircleAvatar( backgroundColor: Theme.of(context).primaryColor, @@ -62,7 +62,7 @@ class SelectUserForSharingPage extends HookConsumerWidget { } } - _buildUserList(List users) { + buildUserList(List users) { List usersChip = []; for (var user in sharedUsersList.value) { @@ -104,7 +104,7 @@ class SelectUserForSharingPage extends HookConsumerWidget { shrinkWrap: true, itemBuilder: ((context, index) { return ListTile( - leading: _buildTileIcon(users[index]), + leading: buildTileIcon(users[index]), title: Text( users[index].email, style: const TextStyle( @@ -153,8 +153,7 @@ class SelectUserForSharingPage extends HookConsumerWidget { style: TextButton.styleFrom( foregroundColor: Theme.of(context).primaryColor, ), - onPressed: - sharedUsersList.value.isEmpty ? null : _createSharedAlbum, + onPressed: sharedUsersList.value.isEmpty ? null : createSharedAlbum, child: const Text( "share_create_album", style: TextStyle( @@ -168,7 +167,7 @@ class SelectUserForSharingPage extends HookConsumerWidget { ), body: suggestedShareUsers.when( data: (users) { - return _buildUserList(users); + return buildUserList(users); }, error: (e, _) => Text("Error loading suggested users $e"), loading: () => const Center( diff --git a/mobile/lib/modules/album/views/sharing_page.dart b/mobile/lib/modules/album/views/sharing_page.dart index cb04ebf759..492e141468 100644 --- a/mobile/lib/modules/album/views/sharing_page.dart +++ b/mobile/lib/modules/album/views/sharing_page.dart @@ -28,7 +28,7 @@ class SharingPage extends HookConsumerWidget { [], ); - _buildAlbumList() { + buildAlbumList() { return SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { @@ -71,7 +71,7 @@ class SharingPage extends HookConsumerWidget { ); } - _buildEmptyListIndication() { + buildEmptyListIndication() { return SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(8.0), @@ -136,8 +136,8 @@ class SharingPage extends HookConsumerWidget { ), ), sharedAlbums.isNotEmpty - ? _buildAlbumList() - : _buildEmptyListIndication() + ? buildAlbumList() + : buildEmptyListIndication() ], ), ); diff --git a/mobile/lib/modules/asset_viewer/providers/image_viewer_page_state.provider.dart b/mobile/lib/modules/asset_viewer/providers/image_viewer_page_state.provider.dart index 6d3ae83842..8bedafc762 100644 --- a/mobile/lib/modules/asset_viewer/providers/image_viewer_page_state.provider.dart +++ b/mobile/lib/modules/asset_viewer/providers/image_viewer_page_state.provider.dart @@ -64,5 +64,7 @@ class ImageViewerStateNotifier extends StateNotifier { final imageViewerStateProvider = StateNotifierProvider( ((ref) => ImageViewerStateNotifier( - ref.watch(imageViewerServiceProvider), ref.watch(shareServiceProvider))), + ref.watch(imageViewerServiceProvider), + ref.watch(shareServiceProvider), + )), ); diff --git a/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart b/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart index bb88d54e60..483deedc23 100644 --- a/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart +++ b/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart @@ -15,7 +15,7 @@ class ExifBottomSheet extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - _buildMap() { + buildMap() { return Padding( padding: const EdgeInsets.symmetric(vertical: 16.0), child: Container( @@ -66,7 +66,7 @@ class ExifBottomSheet extends ConsumerWidget { ExifResponseDto? exifInfo = assetDetail.remote?.exifInfo; - _buildLocationText() { + buildLocationText() { return Text( "${exifInfo?.city}, ${exifInfo?.state}", style: TextStyle( @@ -120,11 +120,11 @@ class ExifBottomSheet extends ConsumerWidget { ).tr(), if (assetDetail.latitude != null && assetDetail.longitude != null) - _buildMap(), + buildMap(), if (exifInfo != null && exifInfo.city != null && exifInfo.state != null) - _buildLocationText(), + buildLocationText(), Text( "${assetDetail.latitude?.toStringAsFixed(4)}, ${assetDetail.longitude?.toStringAsFixed(4)}", style: TextStyle(fontSize: 12, color: Colors.grey[400]), diff --git a/mobile/lib/modules/backup/background_service/localization.dart b/mobile/lib/modules/backup/background_service/localization.dart index 1e8c9142ed..a0c1610ece 100644 --- a/mobile/lib/modules/backup/background_service/localization.dart +++ b/mobile/lib/modules/backup/background_service/localization.dart @@ -1,3 +1,5 @@ +// ignore_for_file: implementation_imports + import 'package:flutter/foundation.dart'; import 'package:easy_localization/src/asset_loader.dart'; import 'package:easy_localization/src/easy_localization_controller.dart'; diff --git a/mobile/lib/modules/backup/ui/album_info_card.dart b/mobile/lib/modules/backup/ui/album_info_card.dart index 51b48f8fe2..952d4393ed 100644 --- a/mobile/lib/modules/backup/ui/album_info_card.dart +++ b/mobile/lib/modules/backup/ui/album_info_card.dart @@ -33,7 +33,7 @@ class AlbumInfoCard extends HookConsumerWidget { ColorFilter unselectedFilter = const ColorFilter.mode(Colors.black, BlendMode.color); - _buildSelectedTextBox() { + buildSelectedTextBox() { if (isSelected) { return Chip( visualDensity: VisualDensity.compact, @@ -67,7 +67,7 @@ class AlbumInfoCard extends HookConsumerWidget { return const SizedBox(); } - _buildImageFilter() { + buildImageFilter() { if (isSelected) { return selectedFilter; } else if (isExcluded) { @@ -163,7 +163,7 @@ class AlbumInfoCard extends HookConsumerWidget { topRight: Radius.circular(12), ), image: DecorationImage( - colorFilter: _buildImageFilter(), + colorFilter: buildImageFilter(), image: imageData != null ? MemoryImage(imageData!) : const AssetImage( @@ -177,7 +177,7 @@ class AlbumInfoCard extends HookConsumerWidget { Positioned( bottom: 10, left: 25, - child: _buildSelectedTextBox(), + child: buildSelectedTextBox(), ) ], ), diff --git a/mobile/lib/modules/backup/views/album_preview_page.dart b/mobile/lib/modules/backup/views/album_preview_page.dart index afca75be1f..c236934794 100644 --- a/mobile/lib/modules/backup/views/album_preview_page.dart +++ b/mobile/lib/modules/backup/views/album_preview_page.dart @@ -15,14 +15,16 @@ class AlbumPreviewPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final assets = useState>([]); - _getAssetsInAlbum() async { + getAssetsInAlbum() async { assets.value = await album.getAssetListRange( - start: 0, end: await album.assetCountAsync); + start: 0, + end: await album.assetCountAsync, + ); } useEffect( () { - _getAssetsInAlbum(); + getAssetsInAlbum(); return null; }, [], diff --git a/mobile/lib/modules/backup/views/backup_album_selection_page.dart b/mobile/lib/modules/backup/views/backup_album_selection_page.dart index 59893117e2..c94552ca3f 100644 --- a/mobile/lib/modules/backup/views/backup_album_selection_page.dart +++ b/mobile/lib/modules/backup/views/backup_album_selection_page.dart @@ -27,7 +27,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { [], ); - _buildAlbumSelectionList() { + buildAlbumSelectionList() { if (availableAlbums.isEmpty) { return const Center( child: ImmichLoadingIndicator(), @@ -56,7 +56,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { ); } - _buildSelectedAlbumNameChip() { + buildSelectedAlbumNameChip() { return selectedBackupAlbums.map((album) { void removeSelection() { if (ref.watch(backupProvider).selectedBackupAlbums.length == 1) { @@ -104,7 +104,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { }).toSet(); } - _buildExcludedAlbumNameChip() { + buildExcludedAlbumNameChip() { return excludedBackupAlbums.map((album) { void removeSelection() { ref @@ -177,8 +177,8 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Wrap( children: [ - ..._buildSelectedAlbumNameChip(), - ..._buildExcludedAlbumNameChip() + ...buildSelectedAlbumNameChip(), + ...buildExcludedAlbumNameChip() ], ), ), @@ -286,7 +286,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { Padding( padding: const EdgeInsets.only(bottom: 16.0), - child: _buildAlbumSelectionList(), + child: buildAlbumSelectionList(), ), ], ), diff --git a/mobile/lib/modules/backup/views/backup_controller_page.dart b/mobile/lib/modules/backup/views/backup_controller_page.dart index a734de85f0..1ca866b0df 100644 --- a/mobile/lib/modules/backup/views/backup_controller_page.dart +++ b/mobile/lib/modules/backup/views/backup_controller_page.dart @@ -45,7 +45,7 @@ class BackupControllerPage extends HookConsumerWidget { [], ); - Widget _buildStorageInformation() { + Widget buildStorageInformation() { return ListTile( leading: Icon( Icons.storage_rounded, @@ -84,7 +84,7 @@ class BackupControllerPage extends HookConsumerWidget { ); } - ListTile _buildAutoBackupController() { + ListTile buildAutoBackupController() { var backUpOption = authenticationState.deviceInfo.isAutoBackup ? "backup_controller_page_status_on".tr() : "backup_controller_page_status_off".tr(); @@ -143,7 +143,7 @@ class BackupControllerPage extends HookConsumerWidget { ); } - void _showErrorToUser(String msg) { + void showErrorToUser(String msg) { final snackBar = SnackBar( content: Text( msg.tr(), @@ -153,7 +153,7 @@ class BackupControllerPage extends HookConsumerWidget { ScaffoldMessenger.of(context).showSnackBar(snackBar); } - void _showBatteryOptimizationInfoToUser() { + void showBatteryOptimizationInfoToUser() { showDialog( context: context, barrierDismissible: false, @@ -193,7 +193,7 @@ class BackupControllerPage extends HookConsumerWidget { ); } - ListTile _buildBackgroundBackupController() { + ListTile buildBackgroundBackupController() { final bool isBackgroundEnabled = backupState.backgroundBackup; final bool isWifiRequired = backupState.backupRequireWifi; final bool isChargingRequired = backupState.backupRequireCharging; @@ -238,8 +238,8 @@ class BackupControllerPage extends HookConsumerWidget { .read(backupProvider.notifier) .configureBackgroundBackup( requireWifi: isChecked, - onError: _showErrorToUser, - onBatteryInfo: _showBatteryOptimizationInfoToUser, + onError: showErrorToUser, + onBatteryInfo: showBatteryOptimizationInfoToUser, ) : null, ), @@ -259,8 +259,8 @@ class BackupControllerPage extends HookConsumerWidget { .read(backupProvider.notifier) .configureBackgroundBackup( requireCharging: isChecked, - onError: _showErrorToUser, - onBatteryInfo: _showBatteryOptimizationInfoToUser, + onError: showErrorToUser, + onBatteryInfo: showBatteryOptimizationInfoToUser, ) : null, ), @@ -268,8 +268,8 @@ class BackupControllerPage extends HookConsumerWidget { onPressed: () => ref.read(backupProvider.notifier).configureBackgroundBackup( enabled: !isBackgroundEnabled, - onError: _showErrorToUser, - onBatteryInfo: _showBatteryOptimizationInfoToUser, + onError: showErrorToUser, + onBatteryInfo: showBatteryOptimizationInfoToUser, ), child: Text( isBackgroundEnabled @@ -284,7 +284,7 @@ class BackupControllerPage extends HookConsumerWidget { ); } - Widget _buildSelectedAlbumName() { + Widget buildSelectedAlbumName() { var text = "backup_controller_page_backup_selected".tr(); var albums = ref.watch(backupProvider).selectedBackupAlbums; @@ -323,7 +323,7 @@ class BackupControllerPage extends HookConsumerWidget { } } - Widget _buildExcludedAlbumName() { + Widget buildExcludedAlbumName() { var text = "backup_controller_page_excluded".tr(); var albums = ref.watch(backupProvider).excludedBackupAlbums; @@ -348,7 +348,7 @@ class BackupControllerPage extends HookConsumerWidget { } } - _buildFolderSelectionTile() { + buildFolderSelectionTile() { return Card( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5), // if you need this @@ -374,8 +374,8 @@ class BackupControllerPage extends HookConsumerWidget { "backup_controller_page_to_backup", style: TextStyle(fontSize: 12), ).tr(), - _buildSelectedAlbumName(), - _buildExcludedAlbumName() + buildSelectedAlbumName(), + buildExcludedAlbumName() ], ), ), @@ -398,7 +398,7 @@ class BackupControllerPage extends HookConsumerWidget { ); } - _buildCurrentBackupAssetInfoCard() { + buildCurrentBackupAssetInfoCard() { return ListTile( leading: Icon( Icons.info_outline_rounded, @@ -606,7 +606,7 @@ class BackupControllerPage extends HookConsumerWidget { ), ), ), - _buildFolderSelectionTile(), + buildFolderSelectionTile(), BackupInfoCard( title: "backup_controller_page_total".tr(), subtitle: "backup_controller_page_total_sub".tr(), @@ -624,13 +624,13 @@ class BackupControllerPage extends HookConsumerWidget { "${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}", ), const Divider(), - _buildAutoBackupController(), + buildAutoBackupController(), if (Platform.isAndroid) const Divider(), - if (Platform.isAndroid) _buildBackgroundBackupController(), + if (Platform.isAndroid) buildBackgroundBackupController(), const Divider(), - _buildStorageInformation(), + buildStorageInformation(), const Divider(), - _buildCurrentBackupAssetInfoCard(), + buildCurrentBackupAssetInfoCard(), Padding( padding: const EdgeInsets.only( top: 24, diff --git a/mobile/lib/modules/home/services/asset.service.dart b/mobile/lib/modules/home/services/asset.service.dart index 3dbf0e8d3e..046d703ddb 100644 --- a/mobile/lib/modules/home/services/asset.service.dart +++ b/mobile/lib/modules/home/services/asset.service.dart @@ -11,7 +11,7 @@ import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/providers/api.provider.dart'; import 'package:immich_mobile/shared/services/api.service.dart'; import 'package:openapi/api.dart'; -import 'package:photo_manager/src/types/entity.dart'; +import 'package:photo_manager/photo_manager.dart'; final assetServiceProvider = Provider( (ref) => AssetService( diff --git a/mobile/lib/modules/home/ui/profile_drawer/profile_drawer.dart b/mobile/lib/modules/home/ui/profile_drawer/profile_drawer.dart index d8bb50c329..d984a70af6 100644 --- a/mobile/lib/modules/home/ui/profile_drawer/profile_drawer.dart +++ b/mobile/lib/modules/home/ui/profile_drawer/profile_drawer.dart @@ -15,7 +15,7 @@ class ProfileDrawer extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - _buildSignoutButton() { + buildSignoutButton() { return ListTile( horizontalTitleGap: 0, leading: SizedBox( @@ -46,7 +46,7 @@ class ProfileDrawer extends HookConsumerWidget { ); } - _buildSettingButton() { + buildSettingButton() { return ListTile( horizontalTitleGap: 0, leading: SizedBox( @@ -79,8 +79,8 @@ class ProfileDrawer extends HookConsumerWidget { padding: EdgeInsets.zero, children: [ const ProfileDrawerHeader(), - _buildSettingButton(), - _buildSignoutButton(), + buildSettingButton(), + buildSignoutButton(), ], ), const ServerInfoBox() diff --git a/mobile/lib/modules/home/ui/profile_drawer/profile_drawer_header.dart b/mobile/lib/modules/home/ui/profile_drawer/profile_drawer_header.dart index d8187b8c5e..1c08cb7b71 100644 --- a/mobile/lib/modules/home/ui/profile_drawer/profile_drawer_header.dart +++ b/mobile/lib/modules/home/ui/profile_drawer/profile_drawer_header.dart @@ -25,7 +25,7 @@ class ProfileDrawerHeader extends HookConsumerWidget { var dummmy = Random().nextInt(1024); final isDarkMode = Theme.of(context).brightness == Brightness.dark; - _buildUserProfileImage() { + buildUserProfileImage() { if (authState.profileImagePath.isEmpty) { return const CircleAvatar( radius: 35, @@ -77,7 +77,7 @@ class ProfileDrawerHeader extends HookConsumerWidget { return const SizedBox(); } - _pickUserProfileImage() async { + pickUserProfileImage() async { final XFile? image = await ImagePicker().pickImage( source: ImageSource.gallery, maxHeight: 1024, @@ -98,7 +98,7 @@ class ProfileDrawerHeader extends HookConsumerWidget { useEffect( () { - _buildUserProfileImage(); + buildUserProfileImage(); return null; }, [], @@ -129,12 +129,12 @@ class ProfileDrawerHeader extends HookConsumerWidget { Stack( clipBehavior: Clip.none, children: [ - _buildUserProfileImage(), + buildUserProfileImage(), Positioned( bottom: 0, right: -5, child: GestureDetector( - onTap: _pickUserProfileImage, + onTap: pickUserProfileImage, child: Material( color: Colors.grey[100], elevation: 3, diff --git a/mobile/lib/modules/home/ui/profile_drawer/server_info_box.dart b/mobile/lib/modules/home/ui/profile_drawer/server_info_box.dart index 8cd84fdfce..04edfc5f79 100644 --- a/mobile/lib/modules/home/ui/profile_drawer/server_info_box.dart +++ b/mobile/lib/modules/home/ui/profile_drawer/server_info_box.dart @@ -17,7 +17,7 @@ class ServerInfoBox extends HookConsumerWidget { final appInfo = useState({}); - _getPackageInfo() async { + getPackageInfo() async { PackageInfo packageInfo = await PackageInfo.fromPlatform(); appInfo.value = { @@ -28,7 +28,7 @@ class ServerInfoBox extends HookConsumerWidget { useEffect( () { - _getPackageInfo(); + getPackageInfo(); return null; }, [], diff --git a/mobile/lib/modules/login/providers/oauth.provider.dart b/mobile/lib/modules/login/providers/oauth.provider.dart index 0470d539a5..dc0b1e643d 100644 --- a/mobile/lib/modules/login/providers/oauth.provider.dart +++ b/mobile/lib/modules/login/providers/oauth.provider.dart @@ -2,5 +2,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/login/services/oauth.service.dart'; import 'package:immich_mobile/shared/providers/api.provider.dart'; -final OAuthServiceProvider = +final oAuthServiceProvider = Provider((ref) => OAuthService(ref.watch(apiServiceProvider))); diff --git a/mobile/lib/modules/login/ui/login_form.dart b/mobile/lib/modules/login/ui/login_form.dart index a1c32f80d7..6a0b72a0b8 100644 --- a/mobile/lib/modules/login/ui/login_form.dart +++ b/mobile/lib/modules/login/ui/login_form.dart @@ -349,7 +349,7 @@ class OAuthLoginButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - var oAuthService = ref.watch(OAuthServiceProvider); + var oAuthService = ref.watch(oAuthServiceProvider); void performOAuthLogin() async { ref.watch(assetProvider.notifier).clearAllAsset(); diff --git a/mobile/lib/modules/search/views/search_page.dart b/mobile/lib/modules/search/views/search_page.dart index d80a18a353..14c8202d2e 100644 --- a/mobile/lib/modules/search/views/search_page.dart +++ b/mobile/lib/modules/search/views/search_page.dart @@ -39,14 +39,14 @@ class SearchPage extends HookConsumerWidget { [], ); - _onSearchSubmitted(String searchTerm) async { + onSearchSubmitted(String searchTerm) async { searchFocusNode.unfocus(); ref.watch(searchPageStateProvider.notifier).disableSearch(); AutoRouter.of(context).push(SearchResultRoute(searchTerm: searchTerm)); } - _buildPlaces() { + buildPlaces() { return curatedLocation.when( loading: () => SizedBox( height: imageSize, @@ -97,7 +97,7 @@ class SearchPage extends HookConsumerWidget { ); } - _buildThings() { + buildThings() { return curatedObjects.when( loading: () => SizedBox( height: imageSize, @@ -155,7 +155,7 @@ class SearchPage extends HookConsumerWidget { return Scaffold( appBar: SearchBar( searchFocusNode: searchFocusNode, - onSubmitted: _onSearchSubmitted, + onSubmitted: onSearchSubmitted, ), body: GestureDetector( onTap: () { @@ -174,7 +174,7 @@ class SearchPage extends HookConsumerWidget { style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14), ).tr(), ), - _buildPlaces(), + buildPlaces(), Padding( padding: const EdgeInsets.all(16.0), child: const Text( @@ -182,11 +182,11 @@ class SearchPage extends HookConsumerWidget { style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14), ).tr(), ), - _buildThings() + buildThings() ], ), if (isSearchEnabled) - SearchSuggestionList(onSubmitted: _onSearchSubmitted), + SearchSuggestionList(onSubmitted: onSearchSubmitted), ], ), ), diff --git a/mobile/lib/modules/search/views/search_result_page.dart b/mobile/lib/modules/search/views/search_result_page.dart index 048cf46b9f..4d20f87668 100644 --- a/mobile/lib/modules/search/views/search_result_page.dart +++ b/mobile/lib/modules/search/views/search_result_page.dart @@ -38,7 +38,7 @@ class SearchResultPage extends HookConsumerWidget { [], ); - _onSearchSubmitted(String newSearchTerm) { + onSearchSubmitted(String newSearchTerm) { debugPrint("Re-Search with $newSearchTerm"); searchFocusNode?.unfocus(); isNewSearch.value = false; @@ -46,7 +46,7 @@ class SearchResultPage extends HookConsumerWidget { ref.watch(searchResultPageProvider.notifier).search(newSearchTerm); } - _buildTextField() { + buildTextField() { return TextField( controller: searchTermController, focusNode: searchFocusNode, @@ -60,7 +60,7 @@ class SearchResultPage extends HookConsumerWidget { onSubmitted: (searchTerm) { if (searchTerm.isNotEmpty) { searchTermController.clear(); - _onSearchSubmitted(searchTerm); + onSearchSubmitted(searchTerm); } else { isNewSearch.value = false; } @@ -80,7 +80,7 @@ class SearchResultPage extends HookConsumerWidget { ); } - _buildChip() { + buildChip() { return Chip( label: Wrap( spacing: 5, @@ -108,7 +108,7 @@ class SearchResultPage extends HookConsumerWidget { ); } - _buildSearchResult() { + buildSearchResult() { var searchResultPageState = ref.watch(searchResultPageProvider); var searchResultRenderList = ref.watch(searchRenderListProvider); @@ -154,7 +154,7 @@ class SearchResultPage extends HookConsumerWidget { isNewSearch.value = true; searchFocusNode?.requestFocus(); }, - child: isNewSearch.value ? _buildTextField() : _buildChip(), + child: isNewSearch.value ? buildTextField() : buildChip(), ), centerTitle: false, ), @@ -168,9 +168,9 @@ class SearchResultPage extends HookConsumerWidget { }, child: Stack( children: [ - _buildSearchResult(), + buildSearchResult(), if (isNewSearch.value) - SearchSuggestionList(onSubmitted: _onSearchSubmitted), + SearchSuggestionList(onSubmitted: onSearchSubmitted), ], ), ), diff --git a/mobile/lib/modules/settings/views/settings_page.dart b/mobile/lib/modules/settings/views/settings_page.dart index 9a16793429..8422e201ab 100644 --- a/mobile/lib/modules/settings/views/settings_page.dart +++ b/mobile/lib/modules/settings/views/settings_page.dart @@ -4,7 +4,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_settings.dart'; -import 'package:immich_mobile/modules/settings/ui/experimental_settings/experimental_settings.dart'; import 'package:immich_mobile/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart'; import 'package:immich_mobile/modules/settings/ui/notification_setting/notification_setting.dart'; import 'package:immich_mobile/modules/settings/ui/theme_setting/theme_setting.dart'; diff --git a/mobile/lib/shared/providers/asset.provider.dart b/mobile/lib/shared/providers/asset.provider.dart index 9de766439a..fd84804cbf 100644 --- a/mobile/lib/shared/providers/asset.provider.dart +++ b/mobile/lib/shared/providers/asset.provider.dart @@ -39,7 +39,8 @@ class AssetNotifier extends StateNotifier> { stopwatch.start(); state = await _assetCacheService.get(); debugPrint( - "Reading assets from cache: ${stopwatch.elapsedMilliseconds}ms"); + "Reading assets from cache: ${stopwatch.elapsedMilliseconds}ms", + ); stopwatch.reset(); } @@ -145,7 +146,9 @@ class AssetNotifier extends StateNotifier> { final assetProvider = StateNotifierProvider>((ref) { return AssetNotifier( - ref.watch(assetServiceProvider), ref.watch(assetCacheServiceProvider)); + ref.watch(assetServiceProvider), + ref.watch(assetCacheServiceProvider), + ); }); final assetGroupByDateTimeProvider = StateProvider((ref) { diff --git a/mobile/lib/shared/providers/release_info.provider.dart b/mobile/lib/shared/providers/release_info.provider.dart index da24505e98..fcdd398cc0 100644 --- a/mobile/lib/shared/providers/release_info.provider.dart +++ b/mobile/lib/shared/providers/release_info.provider.dart @@ -17,10 +17,11 @@ class ReleaseInfoNotifier extends StateNotifier { try { String? localReleaseVersion = box.get(githubReleaseInfoKey); final res = await client.get( - Uri.parse( - "https://api.github.com/repos/immich-app/immich/releases/latest", - ), - headers: {"Accept": "application/vnd.github.v3+json"}); + Uri.parse( + "https://api.github.com/repos/immich-app/immich/releases/latest", + ), + headers: {"Accept": "application/vnd.github.v3+json"}, + ); if (res.statusCode == 200) { final data = jsonDecode(res.body); diff --git a/mobile/lib/shared/services/cache.service.dart b/mobile/lib/shared/services/cache.service.dart index a251c14495..72ca31f7d6 100644 --- a/mobile/lib/shared/services/cache.service.dart +++ b/mobile/lib/shared/services/cache.service.dart @@ -1,3 +1,4 @@ +// ignore: depend_on_referenced_packages import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; diff --git a/mobile/lib/shared/ui/immich_toast.dart b/mobile/lib/shared/ui/immich_toast.dart index 1bc0bb4ea8..3f15c13a2a 100644 --- a/mobile/lib/shared/ui/immich_toast.dart +++ b/mobile/lib/shared/ui/immich_toast.dart @@ -15,7 +15,7 @@ class ImmichToast { final fToast = FToast(); fToast.init(context); - Color _getColor(ToastType type, BuildContext context) { + Color getColor(ToastType type, BuildContext context) { switch (type) { case ToastType.info: return Theme.of(context).primaryColor; @@ -26,7 +26,7 @@ class ImmichToast { } } - Icon _getIcon(ToastType type) { + Icon getIcon(ToastType type) { switch (type) { case ToastType.info: return Icon( @@ -60,7 +60,7 @@ class ImmichToast { child: Row( mainAxisSize: MainAxisSize.min, children: [ - _getIcon(toastType), + getIcon(toastType), const SizedBox( width: 12.0, ), @@ -68,7 +68,7 @@ class ImmichToast { child: Text( msg, style: TextStyle( - color: _getColor(toastType, context), + color: getColor(toastType, context), fontWeight: FontWeight.bold, fontSize: 15, ), diff --git a/mobile/lib/utils/immich_cache_info_repository.dart b/mobile/lib/utils/immich_cache_info_repository.dart index 87a17ac0b4..699713a6bd 100644 --- a/mobile/lib/utils/immich_cache_info_repository.dart +++ b/mobile/lib/utils/immich_cache_info_repository.dart @@ -1,3 +1,5 @@ +// ignore_for_file: depend_on_referenced_packages, implementation_imports + import 'dart:io'; import 'dart:math'; From a9320f06e89b4e858596f34a01072705c3c8021c Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 21 Nov 2022 12:53:25 -0600 Subject: [PATCH 12/30] Added v1.36 release note to website --- docs/blog/2019-05-28-first-blog-post.md | 12 -- docs/blog/2019-05-29-long-blog-post.md | 44 ------- docs/blog/2021-08-01-mdx-blog-post.mdx | 20 --- .../docusaurus-plushie-banner.jpeg | Bin 96122 -> 0 bytes docs/blog/2021-08-26-welcome/index.md | 25 ---- docs/blog/authors.yml | 22 +--- docs/blog/release-1.36/index.mdx | 114 ++++++++++++++++++ docs/docs/usage/oauth.md | 2 +- docs/docusaurus.config.js | 2 +- 9 files changed, 121 insertions(+), 120 deletions(-) delete mode 100644 docs/blog/2019-05-28-first-blog-post.md delete mode 100644 docs/blog/2019-05-29-long-blog-post.md delete mode 100644 docs/blog/2021-08-01-mdx-blog-post.mdx delete mode 100644 docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg delete mode 100644 docs/blog/2021-08-26-welcome/index.md create mode 100644 docs/blog/release-1.36/index.mdx diff --git a/docs/blog/2019-05-28-first-blog-post.md b/docs/blog/2019-05-28-first-blog-post.md deleted file mode 100644 index 02f3f81bd2..0000000000 --- a/docs/blog/2019-05-28-first-blog-post.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -slug: first-blog-post -title: First Blog Post -authors: - name: Gao Wei - title: Docusaurus Core Team - url: https://github.com/wgao19 - image_url: https://github.com/wgao19.png -tags: [hola, docusaurus] ---- - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet diff --git a/docs/blog/2019-05-29-long-blog-post.md b/docs/blog/2019-05-29-long-blog-post.md deleted file mode 100644 index 26ffb1b1f6..0000000000 --- a/docs/blog/2019-05-29-long-blog-post.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -slug: long-blog-post -title: Long Blog Post -authors: endi -tags: [hello, docusaurus] ---- - -This is the summary of a very long blog post, - -Use a `` comment to limit blog post size in the list view. - - - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet diff --git a/docs/blog/2021-08-01-mdx-blog-post.mdx b/docs/blog/2021-08-01-mdx-blog-post.mdx deleted file mode 100644 index c04ebe323e..0000000000 --- a/docs/blog/2021-08-01-mdx-blog-post.mdx +++ /dev/null @@ -1,20 +0,0 @@ ---- -slug: mdx-blog-post -title: MDX Blog Post -authors: [slorber] -tags: [docusaurus] ---- - -Blog posts support [Docusaurus Markdown features](https://docusaurus.io/docs/markdown-features), such as [MDX](https://mdxjs.com/). - -:::tip - -Use the power of React to create interactive blog posts. - -```js - -``` - - - -::: diff --git a/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg b/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg deleted file mode 100644 index 11bda0928456b12f8e53d0ba5709212a4058d449..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 96122 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 diff --git a/docs/blog/2021-08-26-welcome/index.md b/docs/blog/2021-08-26-welcome/index.md deleted file mode 100644 index 9455168f17..0000000000 --- a/docs/blog/2021-08-26-welcome/index.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -slug: welcome -title: Welcome -authors: [slorber, yangshun] -tags: [facebook, hello, docusaurus] ---- - -[Docusaurus blogging features](https://docusaurus.io/docs/blog) are powered by the [blog plugin](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog). - -Simply add Markdown files (or folders) to the `blog` directory. - -Regular blog authors can be added to `authors.yml`. - -The blog post date can be extracted from filenames, such as: - -- `2019-05-30-welcome.md` -- `2019-05-30-welcome/index.md` - -A blog post folder can be convenient to co-locate blog post images: - -![Docusaurus Plushie](./docusaurus-plushie-banner.jpeg) - -The blog supports tags as well! - -**And if you don't want a blog**: just delete this directory, and use `blog: false` in your Docusaurus config. diff --git a/docs/blog/authors.yml b/docs/blog/authors.yml index bcb2991563..f331efa927 100644 --- a/docs/blog/authors.yml +++ b/docs/blog/authors.yml @@ -1,17 +1,5 @@ -endi: - name: Endilie Yacop Sucipto - title: Maintainer of Docusaurus - url: https://github.com/endiliey - image_url: https://github.com/endiliey.png - -yangshun: - name: Yangshun Tay - title: Front End Engineer @ Facebook - url: https://github.com/yangshun - image_url: https://github.com/yangshun.png - -slorber: - name: Sébastien Lorber - title: Docusaurus maintainer - url: https://sebastienlorber.com - image_url: https://github.com/slorber.png +alextran: + name: Alex Tran + title: Maintainer of Immich + url: https://github.com/alextran1502 + image_url: https://github.com/alextran1502.png diff --git a/docs/blog/release-1.36/index.mdx b/docs/blog/release-1.36/index.mdx new file mode 100644 index 0000000000..9a9c6a5263 --- /dev/null +++ b/docs/blog/release-1.36/index.mdx @@ -0,0 +1,114 @@ +--- +slug: release-1-36 +title: Release v1.36.0 +authors: [alextran] +tags: [release] +date: 2022-11-10 +--- + +Hello everyone, it is my pleasure to deliver the new release of Immich to you. The team has been working hard to bring you the new features and improvements. This release includes some big features that the community has been asking since the beginning of Immich. We hope you will enjoy it. + +Some notable features are: + +- [OAuth integration](#livephoto-ios-support-) +- [LivePhoto support on iOS](#oauth-integration-) +- User config system + + + +## LivePhoto iOS Support 🎉 + +LivePhoto on iOS is now supported in Immich. + +The motion part will now be uploaded and can be played on the mobile app and the web. + +:::caution + +- The server and the app has to be on version **1.36.x** for the application to work correctly. +- Previous uploaded photos will not be updated automatically, you will have to remove and reupload them if you want to keep the LivePhoto functionality. + +::: + + + +## OAuth Integration 🎉 + +I want to borrow this chance to express my gratitude to [@EnricoBilla](https://github.com/EnricoBilla), who has been the trailblazer for this feature since the beginning days of Immich. His PR has sparked ideas, suggestions, and discussion among the team member on how to integrate this feature successfully into the app. Thank you so much for your work and your time. + +OAuth is now integrated into the system. Please follow the guide [here](https://immich.app/docs/usage/oauth) to set up your OAuth integration + +After setting up the correct environment variables in the `.env` file, as shown below + +| Key | Type | Default | Description | +| ------------------- | ------- | -------------------- | ------------------------------------------------------------------------- | +| OAUTH_ENABLED | boolean | false | Enable/disable OAuth2 | +| OAUTH_ISSUER_URL | URL | (required) | Required. Self-discovery URL for client | +| OAUTH_CLIENT_ID | string | (required) | Required. Client ID | +| OAUTH_CLIENT_SECRET | string | (required) | Required. Client Secret | +| OAUTH_SCOPE | string | openid email profile | Full list of scopes to send with the request (space delimited) | +| OAUTH_AUTO_REGISTER | boolean | true | When true, will automatically register a user the first time they sign in | +| OAUTH_BUTTON_TEXT | string | Login with OAuth | Text for the OAuth button on the web | + +```bash title="Authentik Example" +OAUTH_ENABLED=true +OAUTH_ISSUER_URL=http://10.1.15.216:9000/application/o/immich-test/ +OAUTH_CLIENT_ID=30596v8f78a4b6a97d5985c3076b6b4c4d12ddc33 +OAUTH_CLIENT_SECRET=50f1eafdec353b95b1c638db390db4ab67ef035a51212dbec2f56175e2eb272b5d572c099176e6fe116ecf47ffdd544bgdb9e2edc588307ee0339d25eeccd88 +OAUTH_BUTTON_TEXT=Login with Authentik +``` + +The web will have the option to sign in with OAuth. + + + +The mobile app will check if the server has OAuth enabled before displaying the OAuth +sign-in button. + + + +## Support + + + +If you find the project helpful and it helps you in some ways, you can support the project [one time](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) or [monthly](https://github.com/sponsors/alextran1502) from GitHub Sponsor + +It is a great way to let me know that you want me to continue developing and working on this project for years to come. + +## Details + +For more details, please check out the [release note](https://github.com/immich-app/immich/releases/tag/v1.36.0_55-dev) diff --git a/docs/docs/usage/oauth.md b/docs/docs/usage/oauth.md index 30cd842bff..78eb47aed5 100644 --- a/docs/docs/usage/oauth.md +++ b/docs/docs/usage/oauth.md @@ -49,7 +49,7 @@ Once you have a new OAuth client application configured, Immich can be configure | OAUTH_ENABLED | boolean | false | Enable/disable OAuth2 | | OAUTH_ISSUER_URL | URL | (required) | Required. Self-discovery URL for client (from previous step) | | OAUTH_CLIENT_ID | string | (required) | Required. Client ID (from previous step) | -| OAUTH_CLIENT_SECRET | string | (required) | Required. Client Secret (previous step | +| OAUTH_CLIENT_SECRET | string | (required) | Required. Client Secret (previous step) | | OAUTH_SCOPE | string | openid email profile | Full list of scopes to send with the request (space delimited) | | OAUTH_AUTO_REGISTER | boolean | true | When true, will automatically register a user the first time they sign in | | OAUTH_BUTTON_TEXT | string | Login with OAuth | Text for the OAuth button on the web | diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index d1094f50aa..afb5d61b16 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -80,7 +80,7 @@ const config = { position: "right", label: "Documentation", }, - // { to: "/blog", label: "Blog", position: "right" }, + { to: "/blog", label: "Blog", position: "right" }, { href: "https://github.com/immich-app/immich", label: "GitHub", From 2227a6f5f3b37aae6a2ef213ba1a92a869f85e56 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 21 Nov 2022 13:54:30 -0600 Subject: [PATCH 13/30] Added custom buildscript for XCodeCloud --- mobile/ios/ci_scripts/ci_post_clone.sh | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100755 mobile/ios/ci_scripts/ci_post_clone.sh diff --git a/mobile/ios/ci_scripts/ci_post_clone.sh b/mobile/ios/ci_scripts/ci_post_clone.sh new file mode 100755 index 0000000000..1a4f0cd169 --- /dev/null +++ b/mobile/ios/ci_scripts/ci_post_clone.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +# The default execution directory of this script is the ci_scripts directory. +cd $CI_WORKSPACE/mobile + +# Install Flutter using git. +git clone https://github.com/flutter/flutter.git --depth 1 -b stable $HOME/flutter +export PATH="$PATH:$HOME/flutter/bin" + +# Install Flutter artifacts for iOS (--ios), or macOS (--macos) platforms. +flutter precache --ios + +# Install Flutter dependencies. +flutter pub get + +# Install CocoaPods using Homebrew. +HOMEBREW_NO_AUTO_UPDATE=1 # disable homebrew's automatic updates. +brew install cocoapods + +# Install CocoaPods dependencies. +cd ios && pod install # run `pod install` in the `ios` directory. + +exit 0 From cc697486fcea51bdcab8439b85b83acb08e4248b Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 21 Nov 2022 20:24:56 -0600 Subject: [PATCH 14/30] fix(server): Deleted shared users cause problem with album retrival and creation (#1002) * fix(server): Deleted shared users cause problem with album retrival and creation * Remove dead code --- .../album/response-dto/album-response.dto.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/server/apps/immich/src/api-v1/album/response-dto/album-response.dto.ts b/server/apps/immich/src/api-v1/album/response-dto/album-response.dto.ts index dde9998e3e..71d4475430 100644 --- a/server/apps/immich/src/api-v1/album/response-dto/album-response.dto.ts +++ b/server/apps/immich/src/api-v1/album/response-dto/album-response.dto.ts @@ -18,7 +18,14 @@ export class AlbumResponseDto { } export function mapAlbum(entity: AlbumEntity): AlbumResponseDto { - const sharedUsers = entity.sharedUsers?.map((userAlbum) => mapUser(userAlbum.userInfo)) || []; + const sharedUsers: UserResponseDto[] = []; + + entity.sharedUsers?.forEach((userAlbum) => { + if (userAlbum.userInfo) { + const user = mapUser(userAlbum.userInfo); + sharedUsers.push(user); + } + }); return { albumName: entity.albumName, albumThumbnailAssetId: entity.albumThumbnailAssetId, @@ -33,7 +40,14 @@ export function mapAlbum(entity: AlbumEntity): AlbumResponseDto { } export function mapAlbumExcludeAssetInfo(entity: AlbumEntity): AlbumResponseDto { - const sharedUsers = entity.sharedUsers?.map((userAlbum) => mapUser(userAlbum.userInfo)) || []; + const sharedUsers: UserResponseDto[] = []; + + entity.sharedUsers?.forEach((userAlbum) => { + if (userAlbum.userInfo) { + const user = mapUser(userAlbum.userInfo); + sharedUsers.push(user); + } + }); return { albumName: entity.albumName, albumThumbnailAssetId: entity.albumThumbnailAssetId, From df0a059a02b4d5839d93e1b199a63ba54e2c1f6c Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Mon, 21 Nov 2022 20:26:03 -0600 Subject: [PATCH 15/30] Up patch version --- server/apps/immich/src/constants/server_version.constant.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/apps/immich/src/constants/server_version.constant.ts b/server/apps/immich/src/constants/server_version.constant.ts index 6febc09400..219156c8cb 100644 --- a/server/apps/immich/src/constants/server_version.constant.ts +++ b/server/apps/immich/src/constants/server_version.constant.ts @@ -11,6 +11,6 @@ export interface IServerVersion { export const serverVersion: IServerVersion = { major: 1, minor: 36, - patch: 1, + patch: 2, build: 56, }; From 976d347623ae706f282f72bf866eed8afecb5e47 Mon Sep 17 00:00:00 2001 From: Kiel Hurley Date: Fri, 25 Nov 2022 06:39:27 +1300 Subject: [PATCH 16/30] feat(server,web,mobile): Use binary prefixes for data sizes (#1009) --- .../asset_viewer/ui/exif_bottom_sheet.dart | 5 +- mobile/lib/utils/bytes_units.dart | 28 +++++------ .../src/modules/download/download.service.ts | 2 +- .../immich/src/utils/human-readable.util.ts | 46 ++++++++----------- .../server-stats/server-stats-panel.svelte | 4 +- .../asset-viewer/detail-panel.svelte | 33 ++----------- .../shared-components/upload-panel.svelte | 31 ++----------- web/src/lib/utils/byte-units.ts | 16 +++++++ 8 files changed, 64 insertions(+), 101 deletions(-) create mode 100644 web/src/lib/utils/byte-units.ts diff --git a/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart b/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart index 483deedc23..23dd1ccb0d 100644 --- a/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart +++ b/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart @@ -6,6 +6,7 @@ import 'package:immich_mobile/shared/models/asset.dart'; import 'package:openapi/api.dart'; import 'package:path/path.dart' as p; import 'package:latlong2/latlong.dart'; +import 'package:immich_mobile/utils/bytes_units.dart'; class ExifBottomSheet extends ConsumerWidget { final Asset assetDetail; @@ -162,7 +163,7 @@ class ExifBottomSheet extends ConsumerWidget { ), subtitle: exifInfo.exifImageHeight != null ? Text( - "${exifInfo.exifImageHeight} x ${exifInfo.exifImageWidth} ${exifInfo.fileSizeInByte!}B ", + "${exifInfo.exifImageHeight} x ${exifInfo.exifImageWidth} ${formatBytes(exifInfo.fileSizeInByte!)} ", ) : null, ), @@ -178,7 +179,7 @@ class ExifBottomSheet extends ConsumerWidget { style: const TextStyle(fontWeight: FontWeight.bold), ), subtitle: Text( - "ƒ/${exifInfo.fNumber} 1/${(1 / (exifInfo.exposureTime ?? 1)).toStringAsFixed(0)} ${exifInfo.focalLength}mm ISO${exifInfo.iso} ", + "ƒ/${exifInfo.fNumber} 1/${(1 / (exifInfo.exposureTime ?? 1)).toStringAsFixed(0)} ${exifInfo.focalLength} mm ISO${exifInfo.iso} ", ), ), ], diff --git a/mobile/lib/utils/bytes_units.dart b/mobile/lib/utils/bytes_units.dart index 78e9f17df7..be9ac13dcb 100644 --- a/mobile/lib/utils/bytes_units.dart +++ b/mobile/lib/utils/bytes_units.dart @@ -1,15 +1,17 @@ - String formatBytes(int bytes) { - if (bytes < 1000) { - return "$bytes B"; - } else if (bytes < 1000000) { - final kb = (bytes / 1000).toStringAsFixed(1); - return "$kb kB"; - } else if (bytes < 1000000000) { - final mb = (bytes / 1000000).toStringAsFixed(1); - return "$mb MB"; - } else { - final gb = (bytes / 1000000000).toStringAsFixed(1); - return "$gb GB"; + const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB']; + + int magnitude = 0; + double remainder = bytes.toDouble(); + while (remainder >= 1024) { + if (magnitude + 1 < units.length) { + magnitude++; + remainder /= 1024; + } + else { + break; + } } -} \ No newline at end of file + + return "${remainder.toStringAsFixed(magnitude == 0 ? 0 : 1)} ${units[magnitude]}"; +} diff --git a/server/apps/immich/src/modules/download/download.service.ts b/server/apps/immich/src/modules/download/download.service.ts index 191f9addca..0c47cce429 100644 --- a/server/apps/immich/src/modules/download/download.service.ts +++ b/server/apps/immich/src/modules/download/download.service.ts @@ -35,7 +35,7 @@ export class DownloadService { fileCount++; // for easier testing, can be changed before merging. - if (totalSize > HumanReadableSize.GB * 20) { + if (totalSize > HumanReadableSize.GiB * 20) { complete = false; this.logger.log( `Archive size exceeded after ${fileCount} files, capping at ${totalSize} bytes (${asHumanReadable( diff --git a/server/apps/immich/src/utils/human-readable.util.ts b/server/apps/immich/src/utils/human-readable.util.ts index 6c71d05e20..aa9bb04af8 100644 --- a/server/apps/immich/src/utils/human-readable.util.ts +++ b/server/apps/immich/src/utils/human-readable.util.ts @@ -1,31 +1,25 @@ -const KB = 1000; -const MB = KB * 1000; -const GB = MB * 1000; -const TB = GB * 1000; -const PB = TB * 1000; +const KiB = Math.pow(1024, 1); +const MiB = Math.pow(1024, 2); +const GiB = Math.pow(1024, 3); +const TiB = Math.pow(1024, 4); +const PiB = Math.pow(1024, 5); -export const HumanReadableSize = { KB, MB, GB, TB, PB }; +export const HumanReadableSize = { KiB, MiB, GiB, TiB, PiB }; -export function asHumanReadable(bytes: number, precision = 1) { - if (bytes >= PB) { - return `${(bytes / PB).toFixed(precision)}PB`; - } +export function asHumanReadable(bytes: number, precision = 1): string { + const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB']; - if (bytes >= TB) { - return `${(bytes / TB).toFixed(precision)}TB`; - } + let magnitude = 0; + let remainder = bytes; + while (remainder >= 1024) { + if (magnitude + 1 < units.length) { + magnitude++; + remainder /= 1024; + } + else { + break; + } + } - if (bytes >= GB) { - return `${(bytes / GB).toFixed(precision)}GB`; - } - - if (bytes >= MB) { - return `${(bytes / MB).toFixed(precision)}MB`; - } - - if (bytes >= KB) { - return `${(bytes / KB).toFixed(precision)}KB`; - } - - return `${bytes}B`; + return `${remainder.toFixed( magnitude == 0 ? 0 : precision )} ${units[magnitude]}`; } diff --git a/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte b/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte index bfae8451cc..0ef9184312 100644 --- a/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte +++ b/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte @@ -15,8 +15,8 @@ return name; }; - $: spaceUnit = stats.usage.slice(stats.usage.length - 2, stats.usage.length); - $: spaceUsage = stats.usage.slice(0, stats.usage.length - 2); + $: spaceUnit = stats.usage.split(' ')[1]; + $: spaceUsage = stats.usage.split(' ')[0];
diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index a254224b5e..c427443392 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -8,6 +8,7 @@ import { createEventDispatcher, onMount } from 'svelte'; import { browser } from '$app/environment'; import { AssetResponseDto, AlbumResponseDto } from '@api'; + import { getHumanReadableBytes } from '../../utils/byte-units'; type Leaflet = typeof import('leaflet'); type LeafletMap = import('leaflet').Map; @@ -59,32 +60,6 @@ } const dispatch = createEventDispatcher(); - const getHumanReadableString = (sizeInByte: number) => { - const pepibyte = 1.126 * Math.pow(10, 15); - const tebibyte = 1.1 * Math.pow(10, 12); - const gibibyte = 1.074 * Math.pow(10, 9); - const mebibyte = 1.049 * Math.pow(10, 6); - const kibibyte = 1024; - // Pebibyte - if (sizeInByte >= pepibyte) { - // Pe - return `${(sizeInByte / pepibyte).toFixed(1)}PB`; - } else if (tebibyte <= sizeInByte && sizeInByte < pepibyte) { - // Te - return `${(sizeInByte / tebibyte).toFixed(1)}TB`; - } else if (gibibyte <= sizeInByte && sizeInByte < tebibyte) { - // Gi - return `${(sizeInByte / gibibyte).toFixed(1)}GB`; - } else if (mebibyte <= sizeInByte && sizeInByte < gibibyte) { - // Mega - return `${(sizeInByte / mebibyte).toFixed(1)}MB`; - } else if (kibibyte <= sizeInByte && sizeInByte < mebibyte) { - // Kibi - return `${(sizeInByte / kibibyte).toFixed(1)}KB`; - } else { - return `${sizeInByte}B`; - } - }; const getMegapixel = (width: number, height: number): number | undefined => { const megapixel = Math.round((height * width) / 1_000_000); @@ -143,13 +118,13 @@ {#if asset.exifInfo.exifImageHeight && asset.exifInfo.exifImageWidth} {#if getMegapixel(asset.exifInfo.exifImageHeight, asset.exifInfo.exifImageWidth)}

- {getMegapixel(asset.exifInfo.exifImageHeight, asset.exifInfo.exifImageWidth)}MP + {getMegapixel(asset.exifInfo.exifImageHeight, asset.exifInfo.exifImageWidth)} MP

{/if}

{asset.exifInfo.exifImageHeight} x {asset.exifInfo.exifImageWidth}

{/if} -

{getHumanReadableString(asset.exifInfo.fileSizeInByte)}

+

{getHumanReadableBytes(asset.exifInfo.fileSizeInByte)}

@@ -162,7 +137,7 @@

{asset.exifInfo.make || ''} {asset.exifInfo.model || ''}

-

{`f/${asset.exifInfo.fNumber}` || ''}

+

{`Æ’/${asset.exifInfo.fNumber}` || ''}

{#if asset.exifInfo.exposureTime}

{`1/${Math.floor(1 / asset.exifInfo.exposureTime)}`}

diff --git a/web/src/lib/components/shared-components/upload-panel.svelte b/web/src/lib/components/shared-components/upload-panel.svelte index f33df973d4..0901cc008c 100644 --- a/web/src/lib/components/shared-components/upload-panel.svelte +++ b/web/src/lib/components/shared-components/upload-panel.svelte @@ -6,6 +6,8 @@ import WindowMinimize from 'svelte-material-icons/WindowMinimize.svelte'; import type { UploadAsset } from '$lib/models/upload-asset'; import { notificationController, NotificationType } from './notification/notification'; + import { getHumanReadableBytes } from '../../utils/byte-units'; + let showDetail = true; let uploadLength = 0; @@ -30,33 +32,6 @@ } }; - function getSizeInHumanReadableFormat(sizeInByte: number) { - const pepibyte = 1.126 * Math.pow(10, 15); - const tebibyte = 1.1 * Math.pow(10, 12); - const gibibyte = 1.074 * Math.pow(10, 9); - const mebibyte = 1.049 * Math.pow(10, 6); - const kibibyte = 1024; - // Pebibyte - if (sizeInByte >= pepibyte) { - // Pe - return `${(sizeInByte / pepibyte).toFixed(1)}PB`; - } else if (tebibyte <= sizeInByte && sizeInByte < pepibyte) { - // Te - return `${(sizeInByte / tebibyte).toFixed(1)}TB`; - } else if (gibibyte <= sizeInByte && sizeInByte < tebibyte) { - // Gi - return `${(sizeInByte / gibibyte).toFixed(1)}GB`; - } else if (mebibyte <= sizeInByte && sizeInByte < gibibyte) { - // Mega - return `${(sizeInByte / mebibyte).toFixed(1)}MB`; - } else if (kibibyte <= sizeInByte && sizeInByte < mebibyte) { - // Kibi - return `${(sizeInByte / kibibyte).toFixed(1)}KB`; - } else { - return `${sizeInByte}B`; - } - } - // Reactive action to get thumbnail image of upload asset whenever there is a new one added to the list $: { if ($uploadAssetsStore.length != uploadLength) { @@ -140,7 +115,7 @@ diff --git a/web/src/lib/utils/byte-units.ts b/web/src/lib/utils/byte-units.ts new file mode 100644 index 0000000000..f8fa139fbe --- /dev/null +++ b/web/src/lib/utils/byte-units.ts @@ -0,0 +1,16 @@ +export function getHumanReadableBytes(bytes: number): string { + const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB']; + + let magnitude = 0; + let remainder = bytes; + while (remainder >= 1024) { + if (magnitude + 1 < units.length) { + magnitude++; + remainder /= 1024; + } else { + break; + } + } + + return `${remainder.toFixed(magnitude == 0 ? 0 : 1)} ${units[magnitude]}`; +} From 80d0ddca9af6e992d3d57c1b5167ba581686aa9a Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 24 Nov 2022 15:47:55 -0600 Subject: [PATCH 17/30] fix(mobile): Fix not able to show device asset on Android 13 (#1016) --- mobile/android/app/build.gradle | 2 +- mobile/android/app/src/main/AndroidManifest.xml | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index 5aae5cfa69..cc0de01bad 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -52,7 +52,7 @@ android { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "app.alextran.immich" minSdkVersion 23 - targetSdkVersion flutter.targetSdkVersion + targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index 7363e4999f..b6bf0b4459 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -38,6 +38,9 @@ + + + From 1e9d67ec393b3710eccabfc5b2be7f303da40de5 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 24 Nov 2022 15:50:18 -0600 Subject: [PATCH 18/30] Up mobile version for hotfix release --- mobile/android/fastlane/Fastfile | 4 ++-- .../android/fastlane/metadata/android/en-US/changelogs/57.txt | 2 ++ mobile/pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 mobile/android/fastlane/metadata/android/en-US/changelogs/57.txt diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index e345b9a121..768f3a3378 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 56, - "android.injected.version.name" => "1.36.1", + "android.injected.version.code" => 57, + "android.injected.version.name" => "1.36.2", } ) upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') diff --git a/mobile/android/fastlane/metadata/android/en-US/changelogs/57.txt b/mobile/android/fastlane/metadata/android/en-US/changelogs/57.txt new file mode 100644 index 0000000000..ff056558b8 --- /dev/null +++ b/mobile/android/fastlane/metadata/android/en-US/changelogs/57.txt @@ -0,0 +1,2 @@ +* Show human readable file size in detail view +* Fix permission issue on Android 33 \ No newline at end of file diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index b76702f0fc..0796cc9df8 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: "none" -version: 1.36.1+56 +version: 1.36.2+57 environment: sdk: ">=2.17.0 <3.0.0" From efa7b3ba544373095df5f17bdc5a640cdc86e384 Mon Sep 17 00:00:00 2001 From: denck007 Date: Fri, 25 Nov 2022 20:52:01 -0600 Subject: [PATCH 19/30] Fix(web): navbar color overlap and scroll bar incorrect z index (#1018) * fix(web): Navbar color overlaps tall images * fix(web): Scroll bar date behind navbar when scrubbing (fixes issue #757) --- .../lib/components/asset-viewer/asset-viewer-nav-bar.svelte | 2 +- .../components/shared-components/scrollbar/scrollbar.svelte | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index 93a929fbac..6240545ed7 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -46,7 +46,7 @@
dispatch('goBack')} /> diff --git a/web/src/lib/components/shared-components/scrollbar/scrollbar.svelte b/web/src/lib/components/shared-components/scrollbar/scrollbar.svelte index fdddf6fcd9..38270f531f 100644 --- a/web/src/lib/components/shared-components/scrollbar/scrollbar.svelte +++ b/web/src/lib/components/shared-components/scrollbar/scrollbar.svelte @@ -94,7 +94,7 @@
(isHover = true)} @@ -109,7 +109,7 @@ > {#if isHover}
{hoveredDate?.toLocaleString('default', { month: 'short' })} From 47f5e4134eb0c34e2381c6d4580aac0f24535a62 Mon Sep 17 00:00:00 2001 From: Fynn Petersen-Frey Date: Sat, 26 Nov 2022 17:16:02 +0100 Subject: [PATCH 20/30] feat(mobile): use cached asset info if unchanged instead of downloading all assets (#1017) * feat(mobile): use cached asset info if unchanged instead of downloading all assets This adds an HTTP ETag to the getAllAssets endpoint and client-side support in the app. If locally cache content is identical to the content on the server, the potentially large list of all assets does not need to be downloaded. * use ts import instead of require --- mobile/lib/constants/hive_box.dart | 1 + .../modules/home/services/asset.service.dart | 57 +++++++----------- .../lib/shared/providers/asset.provider.dart | 49 ++++++++++++--- mobile/lib/utils/openapi_extensions.dart | 53 ++++++++++++++++ mobile/lib/utils/tuple.dart | 8 +++ mobile/openapi/doc/AssetApi.md | Bin 27573 -> 27832 bytes mobile/openapi/doc/AssetResponseDto.md | Bin 1067 -> 1089 bytes mobile/openapi/doc/UserResponseDto.md | Bin 695 -> 706 bytes mobile/openapi/lib/api/asset_api.dart | Bin 33589 -> 33993 bytes .../openapi/lib/model/asset_response_dto.dart | Bin 9208 -> 8947 bytes .../openapi/lib/model/user_response_dto.dart | Bin 5742 -> 5910 bytes .../src/api-v1/asset/asset.controller.ts | 30 +++++++-- .../asset/response-dto/asset-response.dto.ts | 4 +- .../user/response-dto/user-response.dto.ts | 4 +- server/apps/immich/src/types/index.d.ts | 5 ++ server/apps/immich/src/utils/etag.ts | 10 +++ server/immich-openapi-specs.json | 2 +- web/src/api/open-api/api.ts | 28 ++++++--- 18 files changed, 187 insertions(+), 64 deletions(-) create mode 100644 mobile/lib/utils/openapi_extensions.dart create mode 100644 mobile/lib/utils/tuple.dart create mode 100644 server/apps/immich/src/types/index.d.ts create mode 100644 server/apps/immich/src/utils/etag.ts diff --git a/mobile/lib/constants/hive_box.dart b/mobile/lib/constants/hive_box.dart index 5a65c248aa..9db1755053 100644 --- a/mobile/lib/constants/hive_box.dart +++ b/mobile/lib/constants/hive_box.dart @@ -4,6 +4,7 @@ const String accessTokenKey = "immichBoxAccessTokenKey"; // Key 1 const String deviceIdKey = 'immichBoxDeviceIdKey'; // Key 2 const String isLoggedInKey = 'immichIsLoggedInKey'; // Key 3 const String serverEndpointKey = 'immichBoxServerEndpoint'; // Key 4 +const String assetEtagKey = 'immichAssetEtagKey'; // Key 5 // Login Info const String hiveLoginInfoBox = "immichLoginInfoBox"; // Box diff --git a/mobile/lib/modules/home/services/asset.service.dart b/mobile/lib/modules/home/services/asset.service.dart index 046d703ddb..264f636e71 100644 --- a/mobile/lib/modules/home/services/asset.service.dart +++ b/mobile/lib/modules/home/services/asset.service.dart @@ -10,8 +10,9 @@ import 'package:immich_mobile/modules/backup/services/backup.service.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/providers/api.provider.dart'; import 'package:immich_mobile/shared/services/api.service.dart'; +import 'package:immich_mobile/utils/openapi_extensions.dart'; +import 'package:immich_mobile/utils/tuple.dart'; import 'package:openapi/api.dart'; -import 'package:photo_manager/photo_manager.dart'; final assetServiceProvider = Provider( (ref) => AssetService( @@ -28,39 +29,22 @@ class AssetService { AssetService(this._apiService, this._backupService, this._backgroundService); - /// Returns all local, remote assets in that order - Future> getAllAsset({bool urgent = false}) async { - final List assets = []; - try { - // not using `await` here to fetch local & remote assets concurrently - final Future?> remoteTask = - _apiService.assetApi.getAllAssets(); - final Iterable newLocalAssets; - final List localAssets = await _getLocalAssets(urgent); - final List remoteAssets = await remoteTask ?? []; - if (remoteAssets.isNotEmpty && localAssets.isNotEmpty) { - final String deviceId = Hive.box(userInfoBox).get(deviceIdKey); - final Set existingIds = remoteAssets - .where((e) => e.deviceId == deviceId) - .map((e) => e.deviceAssetId) - .toSet(); - newLocalAssets = localAssets.where((e) => !existingIds.contains(e.id)); - } else { - newLocalAssets = localAssets; - } - - assets.addAll(newLocalAssets.map((e) => Asset.local(e))); - // the order (first all local, then remote assets) is important! - assets.addAll(remoteAssets.map((e) => Asset.remote(e))); - } catch (e) { - debugPrint("Error [getAllAsset] ${e.toString()}"); + /// Returns `null` if the server state did not change, else list of assets + Future?> getRemoteAssets() async { + final Box box = Hive.box(userInfoBox); + final Pair, String?>? remote = await _apiService + .assetApi + .getAllAssetsWithETag(eTag: box.get(assetEtagKey)); + if (remote == null) { + return null; } - return assets; + box.put(assetEtagKey, remote.second); + return remote.first.map(Asset.remote).toList(growable: false); } /// if [urgent] is `true`, do not block by waiting on the background service - /// to finish running. Returns an empty list instead after a timeout. - Future> _getLocalAssets(bool urgent) async { + /// to finish running. Returns `null` instead after a timeout. + Future?> getLocalAssets({bool urgent = false}) async { try { final Future hasAccess = urgent ? _backgroundService.hasAccess @@ -71,15 +55,16 @@ class AssetService { } final box = await Hive.openBox(hiveBackupInfoBox); final HiveBackupAlbums? backupAlbumInfo = box.get(backupInfoKey); - - return backupAlbumInfo != null - ? await _backupService - .buildUploadCandidates(backupAlbumInfo.deepCopy()) - : []; + if (backupAlbumInfo != null) { + return (await _backupService + .buildUploadCandidates(backupAlbumInfo.deepCopy())) + .map(Asset.local) + .toList(growable: false); + } } catch (e) { debugPrint("Error [_getLocalAssets] ${e.toString()}"); - return []; } + return null; } Future getAssetById(String assetId) async { diff --git a/mobile/lib/shared/providers/asset.provider.dart b/mobile/lib/shared/providers/asset.provider.dart index fd84804cbf..39df1c58b4 100644 --- a/mobile/lib/shared/providers/asset.provider.dart +++ b/mobile/lib/shared/providers/asset.provider.dart @@ -1,7 +1,9 @@ import 'dart:collection'; import 'package:flutter/foundation.dart'; +import 'package:hive/hive.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/hive_box.dart'; import 'package:immich_mobile/modules/home/services/asset.service.dart'; import 'package:immich_mobile/modules/home/services/asset_cache.service.dart'; import 'package:immich_mobile/shared/models/asset.dart'; @@ -33,10 +35,11 @@ class AssetNotifier extends StateNotifier> { final stopwatch = Stopwatch(); try { _getAllAssetInProgress = true; - final bool isCacheValid = await _assetCacheService.isValid(); + stopwatch.start(); + final localTask = _assetService.getLocalAssets(urgent: !isCacheValid); + final remoteTask = _assetService.getRemoteAssets(); if (isCacheValid && state.isEmpty) { - stopwatch.start(); state = await _assetCacheService.get(); debugPrint( "Reading assets from cache: ${stopwatch.elapsedMilliseconds}ms", @@ -44,21 +47,49 @@ class AssetNotifier extends StateNotifier> { stopwatch.reset(); } - stopwatch.start(); - var allAssets = await _assetService.getAllAsset(urgent: !isCacheValid); - debugPrint("Query assets from API: ${stopwatch.elapsedMilliseconds}ms"); + int remoteBegin = state.indexWhere((a) => a.isRemote); + remoteBegin = remoteBegin == -1 ? state.length : remoteBegin; + final List currentLocal = state.slice(0, remoteBegin); + List? newRemote = await remoteTask; + List? newLocal = await localTask; + debugPrint("Load assets: ${stopwatch.elapsedMilliseconds}ms"); stopwatch.reset(); - - state = allAssets; + if (newRemote == null && + (newLocal == null || currentLocal.equals(newLocal))) { + debugPrint("state is already up-to-date"); + return; + } + newRemote ??= state.slice(remoteBegin); + newLocal ??= []; + state = _combineLocalAndRemoteAssets(local: newLocal, remote: newRemote); + debugPrint("Combining assets: ${stopwatch.elapsedMilliseconds}ms"); } finally { _getAllAssetInProgress = false; } debugPrint("[getAllAsset] setting new asset state"); - stopwatch.start(); + stopwatch.reset(); _cacheState(); debugPrint("Store assets in cache: ${stopwatch.elapsedMilliseconds}ms"); - stopwatch.reset(); + } + + List _combineLocalAndRemoteAssets({ + required Iterable local, + required List remote, + }) { + final List assets = []; + if (remote.isNotEmpty && local.isNotEmpty) { + final String deviceId = Hive.box(userInfoBox).get(deviceIdKey); + final Set existingIds = remote + .where((e) => e.deviceId == deviceId) + .map((e) => e.deviceAssetId) + .toSet(); + local = local.where((e) => !existingIds.contains(e.id)); + } + assets.addAll(local); + // the order (first all local, then remote assets) is important! + assets.addAll(remote); + return assets; } clearAllAsset() { diff --git a/mobile/lib/utils/openapi_extensions.dart b/mobile/lib/utils/openapi_extensions.dart new file mode 100644 index 0000000000..2959be3d10 --- /dev/null +++ b/mobile/lib/utils/openapi_extensions.dart @@ -0,0 +1,53 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:http/http.dart'; +import 'package:openapi/api.dart'; + +import 'tuple.dart'; + +/// Extension methods to retrieve ETag together with the API call +extension WithETag on AssetApi { + /// Get all AssetEntity belong to the user + /// + /// Parameters: + /// + /// * [String] eTag: + /// ETag of data already cached on the client + Future, String?>?> getAllAssetsWithETag({ + String? eTag, + }) async { + final response = await getAllAssetsWithHttpInfo( + ifNoneMatch: eTag, + ); + 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) { + final responseBody = await _decodeBodyBytes(response); + final etag = response.headers[HttpHeaders.etagHeader]; + final data = (await apiClient.deserializeAsync( + responseBody, 'List') as List) + .cast() + .toList(); + return Pair(data, etag); + } + return null; + } +} + +/// Returns the decoded body as UTF-8 if the given headers indicate an 'application/json' +/// content type. Otherwise, returns the decoded body as decoded by dart:http package. +Future _decodeBodyBytes(Response response) async { + final contentType = response.headers['content-type']; + return contentType != null && + contentType.toLowerCase().startsWith('application/json') + ? response.bodyBytes.isEmpty + ? '' + : utf8.decode(response.bodyBytes) + : response.body; +} diff --git a/mobile/lib/utils/tuple.dart b/mobile/lib/utils/tuple.dart new file mode 100644 index 0000000000..5473e9ce43 --- /dev/null +++ b/mobile/lib/utils/tuple.dart @@ -0,0 +1,8 @@ +/// An immutable pair or 2-tuple +/// TODO replace with Record once Dart 2.19 is available +class Pair { + final T1 first; + final T2 second; + + const Pair(this.first, this.second); +} diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index b374febcebc276a1ee1817eb7de75f53179f136d..9140b2d5e89ded507a634df9642dbca0a7a26866 100644 GIT binary patch delta 225 zcmdmbopHxa#tmi4+?i>9`FW|ni6zMyo6D4$m?j_KWRpY^RIpV*;!T!!5z}-HNlaJB zPg6)qEJ;*I%qdDuOsP~zPE5{7O;O0tQz*$uRY=asOwB8qY_Fvwl9S_DT%1}`tbuII zL;a9RY8sRIBU4?GgtfF3Y814zf=h}r^U}4nY7~gK>7uL3WGlmolNG#QZGNnN$r%85 Cs!@jk delta 43 zcmV+`0M!4u*#WiJ0kC!>vwa*r3(nEiP B5$^y1 diff --git a/mobile/openapi/doc/AssetResponseDto.md b/mobile/openapi/doc/AssetResponseDto.md index f6e52a709e6e135fe62af8ba10bec0042ed06964..3e80e12c3f2f250758e2be586104d1a482e7c6dd 100644 GIT binary patch delta 37 jcmZ3@agbxfJ|?c{{DP9q{Jg}R*vWgDP9Sqzn3n?p8=ems delta 15 XcmX@ev6^GUKBmbhnT}7M&Abc%Ho6As diff --git a/mobile/openapi/doc/UserResponseDto.md b/mobile/openapi/doc/UserResponseDto.md index 99f2f5245b8500f304102ee45529642e75acdb17..4d6fb1c669c4e0b6d393b4455a9327b6313d830d 100644 GIT binary patch delta 22 dcmdnadWdy{AQM+~enClQeqLfu>|_C^^HO~iOOi9JxD*uh_4O4L6kJ0R(-rd56jBmP z5)~42ic%9(Dix9wlQU9N6!P;FN-|Ovl5;Xs^GYT=>PSo$RTGt{RtPRB%FIi*S3ojQ zN1=A|M<$=i3PRSC7fAL-A*@nRv{lF}&B@VJs0LaJ0vSL%Qi}o-ixP8-ql?rt({%HI z?$XT#IZQoP!B(LFB$Zl{S`?BGu}TBUrpcX(e<#bui%z!HP@Md@M3+);9WYgw%gJ#p yE>0~e*1+nw$%O$rAfHPkd)Nw@qoc67F*$;1@;!^v$^EKAlTYetY`&nK8V&%n$A)_V delta 102 zcmV-s0Ga>Ei2}8T0-z?7oIAk9d-?q zzhDTHFexFEAZs9#-zg1~kX2 zIPh?Q1O5OHh}okDW1=BJ|Ahw+#uFhS91L$U@#G{==9!siJ~HPIP8W~P2JmNKK6#kl z%@oOVcsuge?3Nk1URWbPa(Q4;w~Mr-qbik#uQGMdp=ML4sZwEBl^WQWZsU`X{FJIF zWDP82%UBnFU*Kz2$8UiVrlZO3Ar}6Cm2u1-VN5o#DCafPbLy@gx(mL;Evhp3vW{JO z6z}CCzR5T7OP+$RlrX1^VMU;;G;pL8@m*=`sPnk5Hn5>i;!u5>Tp#+8DiJ+-9{TO& zz=%38xbc{p)F(5H2t!&4hBkpEO-HPiv8TPmT+RT`S=h?GP0ZnsnVcPlJcvxwqD|2w z3>Wzd^n#1+f-AzF)t~j(0v^Rdo1!4TS|F0its7r5*PKqV62S6h7gAFJW>_}ygo&YRR=Z-(_4y9DV|k7WP= delta 654 zcmb7BO=uHQ5T;2mo42V+QB$i)nNZB`#*oz77BNH*RdYL zpGZv+{9fA@-16t6k79=y1%~(W6Z_%M_*7UU*K2T5_PQIKYBdV6P#E$jl`Q6z8Pt^m zwiF#tm1%rbO6V&^Tjq${o;(*=+HWr2DJct?1eR zzt_W;Tn^K;dto?QuomukqFdfHo9$3LCsIj!5IqvfPYy-L|;9 bWO@y4z|oraCSPi}o*SMlT4B9cWj*#6k5a&P diff --git a/mobile/openapi/lib/model/user_response_dto.dart b/mobile/openapi/lib/model/user_response_dto.dart index 63c176378fe413d055c07013c9612d29f6868e7a..432ec9b1595efb653436930ef6462a22be271687 100644 GIT binary patch delta 721 zcmZ`%O=}ZT6eS-s#u?UYmzk?9E2zr1W zuj_S0=qVq%p2VfNqfp1vz*HGI2m&1JifAT77z!C7R3h#9y@g6?0m-mYIC-HR*VRlfEd z3_v)}G4B)y?EF6^E^grS40C94ez7tO)2-CK0x;r-9Eq&NxDASf3aT)f(5qA_w^-FY z2U;y=Lb+_ww^WJFq_^ot`U*Ya@kRP3y-PRgC|#tYahF_UhYpPOdC1>vZ4DQrq05srhjJ*}R^hRA!S3nJv1L*`dA6HqCf^ zow-e4GOc;;?1yAlXw5HQsZ?Q~YvD%U&y{IyrANLhg1E61ud*_u^rassQy>86Z*`+5p zv4=oiyZIWs0i$qyR&jn_bdh>yih8U9$g$R3lVv#-Cp&PePEO}kp4j)t7(l;^O08! zD7l%9Zzm%UR1_RsllSxgHUbNzROTh-W+p3uT%`jwJ}EyxM { - return await this.assetService.getAllAssets(authUser); + @ApiHeader({ + name: 'if-none-match', + description: 'ETag of data already cached on the client', + required: false, + schema: { type: 'string' }, + }) + @ApiResponse({ + status: 200, + headers: { ETag: { required: true, schema: { type: 'string' } } }, + type: [AssetResponseDto], + }) + async getAllAssets(@GetAuthUser() authUser: AuthUserDto, @Response() response: Res, @Request() request: Req) { + const assets = await this.assetService.getAllAssets(authUser); + const clientEtag = request.headers['if-none-match']; + const json = JSON.stringify(assets); + const serverEtag = await etag(json); + response.setHeader('ETag', serverEtag); + if (clientEtag === serverEtag) { + response.status(304).end(); + } else { + response.contentType('application/json').status(200).send(json); + } } @Post('/time-bucket') diff --git a/server/apps/immich/src/api-v1/asset/response-dto/asset-response.dto.ts b/server/apps/immich/src/api-v1/asset/response-dto/asset-response.dto.ts index 4054d670aa..09d61aae51 100644 --- a/server/apps/immich/src/api-v1/asset/response-dto/asset-response.dto.ts +++ b/server/apps/immich/src/api-v1/asset/response-dto/asset-response.dto.ts @@ -19,10 +19,10 @@ export class AssetResponseDto { mimeType!: string | null; duration!: string; webpPath!: string | null; - encodedVideoPath!: string | null; + encodedVideoPath?: string | null; exifInfo?: ExifResponseDto; smartInfo?: SmartInfoResponseDto; - livePhotoVideoId!: string | null; + livePhotoVideoId?: string | null; } export function mapAsset(entity: AssetEntity): AssetResponseDto { diff --git a/server/apps/immich/src/api-v1/user/response-dto/user-response.dto.ts b/server/apps/immich/src/api-v1/user/response-dto/user-response.dto.ts index 9308a3fead..145b068414 100644 --- a/server/apps/immich/src/api-v1/user/response-dto/user-response.dto.ts +++ b/server/apps/immich/src/api-v1/user/response-dto/user-response.dto.ts @@ -9,7 +9,7 @@ export class UserResponseDto { profileImagePath!: string; shouldChangePassword!: boolean; isAdmin!: boolean; - deletedAt!: Date | null; + deletedAt?: Date; } export function mapUser(entity: UserEntity): UserResponseDto { @@ -22,6 +22,6 @@ export function mapUser(entity: UserEntity): UserResponseDto { profileImagePath: entity.profileImagePath, shouldChangePassword: entity.shouldChangePassword, isAdmin: entity.isAdmin, - deletedAt: entity.deletedAt || null, + deletedAt: entity.deletedAt, }; } diff --git a/server/apps/immich/src/types/index.d.ts b/server/apps/immich/src/types/index.d.ts new file mode 100644 index 0000000000..ad4d7460b8 --- /dev/null +++ b/server/apps/immich/src/types/index.d.ts @@ -0,0 +1,5 @@ +declare module 'crypto' { + namespace webcrypto { + const subtle: SubtleCrypto; + } +} \ No newline at end of file diff --git a/server/apps/immich/src/utils/etag.ts b/server/apps/immich/src/utils/etag.ts new file mode 100644 index 0000000000..294695613f --- /dev/null +++ b/server/apps/immich/src/utils/etag.ts @@ -0,0 +1,10 @@ +import { webcrypto } from 'node:crypto'; +const { subtle } = webcrypto; + +export async function etag(text: string): Promise { + const encoder = new TextEncoder(); + const data = encoder.encode(text); + const buffer = await subtle.digest('SHA-1', data); + const hash = Buffer.from(buffer).toString('base64').slice(0, 27); + return `"${data.length}-${hash}"`; +} \ No newline at end of file diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index fcc51b3e6d..d4961228c6 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -1 +1 @@ -{"openapi":"3.0.0","paths":{"/user":{"get":{"operationId":"getAllUsers","parameters":[{"name":"isAll","required":true,"in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}}}}}},"tags":["User"],"security":[{"bearer":[]}]},"post":{"operationId":"createUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]},"put":{"operationId":"updateUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/info/{userId}":{"get":{"operationId":"getUserById","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"]}},"/user/me":{"get":{"operationId":"getMyUserInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/count":{"get":{"operationId":"getUserCount","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserCountResponseDto"}}}}},"tags":["User"]}},"/user/{userId}":{"delete":{"operationId":"deleteUser","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/{userId}/restore":{"post":{"operationId":"restoreUser","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/profile-image":{"post":{"operationId":"createProfileImage","parameters":[],"requestBody":{"required":true,"description":"A new avatar for the user","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/CreateProfileImageDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProfileImageResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/profile-image/{userId}":{"get":{"operationId":"getProfileImage","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["User"]}},"/asset/upload":{"post":{"operationId":"uploadFile","parameters":[],"requestBody":{"required":true,"description":"Asset Upload Information","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/AssetFileUploadDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetFileUploadResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/download/{assetId}":{"get":{"operationId":"downloadFile","parameters":[{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}},{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/download-library":{"get":{"operationId":"downloadLibrary","parameters":[{"name":"skip","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/file/{assetId}":{"get":{"operationId":"serveFile","parameters":[{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}},{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/thumbnail/{assetId}":{"get":{"operationId":"getAssetThumbnail","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}},{"name":"format","required":false,"in":"query","schema":{"$ref":"#/components/schemas/ThumbnailFormat"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/curated-objects":{"get":{"operationId":"getCuratedObjects","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedObjectsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/curated-locations":{"get":{"operationId":"getCuratedLocations","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedLocationsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search-terms":{"get":{"operationId":"getAssetSearchTerms","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search":{"post":{"operationId":"searchAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchAssetDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/count-by-time-bucket":{"post":{"operationId":"getAssetCountByTimeBucket","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAssetCountByTimeBucketDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCountByTimeBucketResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/count-by-user-id":{"get":{"operationId":"getAssetCountByUserId","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCountByUserIdResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset":{"get":{"operationId":"getAllAssets","summary":"","description":"Get all AssetEntity belong to the user","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DeleteAssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/time-bucket":{"post":{"operationId":"getAssetByTimeBucket","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAssetByTimeBucketDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/{deviceId}":{"get":{"operationId":"getUserAssetsByDeviceId","summary":"","description":"Get all asset of a device that are in the database, ID only.","parameters":[{"name":"deviceId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/assetById/{assetId}":{"get":{"operationId":"getAssetById","summary":"","description":"Get a single asset's information","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]},"put":{"operationId":"updateAssetById","summary":"","description":"Update an asset","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/check":{"post":{"operationId":"checkDuplicateAsset","summary":"","description":"Check duplicated asset before uploading - for Web upload used","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/exist":{"post":{"operationId":"checkExistingAssets","summary":"","description":"Checks if multiple assets exist on the server and returns all existing - used by background backup","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckExistingAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckExistingAssetsResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/auth/login":{"post":{"operationId":"login","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginCredentialDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponseDto"}}}}},"tags":["Authentication"]}},"/auth/admin-sign-up":{"post":{"operationId":"adminSignUp","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignUpDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSignupResponseDto"}}}},"400":{"description":"The server already has an admin"}},"tags":["Authentication"]}},"/auth/validateToken":{"post":{"operationId":"validateAccessToken","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateAccessTokenResponseDto"}}}}},"tags":["Authentication"],"security":[{"bearer":[]}]}},"/auth/logout":{"post":{"operationId":"logout","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogoutResponseDto"}}}}},"tags":["Authentication"]}},"/oauth/config":{"post":{"operationId":"generateConfig","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthConfigDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthConfigResponseDto"}}}}},"tags":["OAuth"]}},"/oauth/callback":{"post":{"operationId":"callback","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthCallbackDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponseDto"}}}}},"tags":["OAuth"]}},"/device-info":{"post":{"operationId":"createDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDeviceInfoDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDeviceInfoDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]}},"/server-info":{"get":{"operationId":"getServerInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerInfoResponseDto"}}}}},"tags":["Server Info"]}},"/server-info/ping":{"get":{"operationId":"pingServer","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerPingResponse"}}}}},"tags":["Server Info"]}},"/server-info/version":{"get":{"operationId":"getServerVersion","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerVersionReponseDto"}}}}},"tags":["Server Info"]}},"/server-info/stats":{"get":{"operationId":"getStats","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerStatsResponseDto"}}}}},"tags":["Server Info"]}},"/album/count-by-user-id":{"get":{"operationId":"getAlbumCountByUserId","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumCountResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album":{"post":{"operationId":"createAlbum","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAlbumDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"get":{"operationId":"getAllAlbums","parameters":[{"name":"shared","required":false,"in":"query","schema":{"type":"boolean"}},{"name":"assetId","required":false,"in":"query","description":"Only returns albums that contain the asset\nIgnores the shared parameter\nundefined: get all albums","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/users":{"put":{"operationId":"addUsersToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddUsersDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/assets":{"put":{"operationId":"addAssetsToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddAssetsResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"removeAssetFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}":{"get":{"operationId":"getAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAlbumDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/user/{userId}":{"delete":{"operationId":"removeUserFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}},{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/download":{"get":{"operationId":"downloadArchive","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}},{"name":"skip","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/jobs":{"get":{"operationId":"getAllJobsStatus","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AllJobStatusResponseDto"}}}}},"tags":["Job"],"security":[{"bearer":[]}]}},"/jobs/{jobId}":{"get":{"operationId":"getJobStatus","parameters":[{"name":"jobId","required":true,"in":"path","schema":{"$ref":"#/components/schemas/JobId"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobStatusResponseDto"}}}}},"tags":["Job"],"security":[{"bearer":[]}]},"put":{"operationId":"sendJobCommand","parameters":[{"name":"jobId","required":true,"in":"path","schema":{"$ref":"#/components/schemas/JobId"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobCommandDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"number"}}}}},"tags":["Job"],"security":[{"bearer":[]}]}},"/system-config":{"get":{"operationId":"getConfig","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemConfigResponseDto"}}}}},"tags":["System Config"],"security":[{"bearer":[]}]},"put":{"operationId":"updateConfig","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSystemConfigDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemConfigResponseDto"}}}}},"tags":["System Config"],"security":[{"bearer":[]}]}}},"info":{"title":"Immich","description":"Immich API","version":"1.17.0","contact":{}},"tags":[],"servers":[{"url":"/api"}],"components":{"securitySchemes":{"bearer":{"scheme":"Bearer","bearerFormat":"JWT","type":"http","name":"JWT","description":"Enter JWT token","in":"header"}},"schemas":{"UserResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"},"profileImagePath":{"type":"string"},"shouldChangePassword":{"type":"boolean"},"isAdmin":{"type":"boolean"},"deletedAt":{"format":"date-time","type":"string","nullable":true}},"required":["id","email","firstName","lastName","createdAt","profileImagePath","shouldChangePassword","isAdmin","deletedAt"]},"CreateUserDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"John"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"UserCountResponseDto":{"type":"object","properties":{"userCount":{"type":"integer"}},"required":["userCount"]},"UpdateUserDto":{"type":"object","properties":{"id":{"type":"string"},"password":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"isAdmin":{"type":"boolean"},"shouldChangePassword":{"type":"boolean"},"profileImagePath":{"type":"string"}},"required":["id"]},"CreateProfileImageDto":{"type":"object","properties":{"file":{"type":"string","format":"binary"}},"required":["file"]},"CreateProfileImageResponseDto":{"type":"object","properties":{"userId":{"type":"string"},"profileImagePath":{"type":"string"}},"required":["userId","profileImagePath"]},"AssetFileUploadDto":{"type":"object","properties":{"assetData":{"type":"string","format":"binary"}},"required":["assetData"]},"AssetFileUploadResponseDto":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]},"ThumbnailFormat":{"type":"string","enum":["JPEG","WEBP"]},"CuratedObjectsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","object","resizePath","deviceAssetId","deviceId"]},"CuratedLocationsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"city":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","city","resizePath","deviceAssetId","deviceId"]},"SearchAssetDto":{"type":"object","properties":{"searchTerm":{"type":"string"}},"required":["searchTerm"]},"AssetTypeEnum":{"type":"string","enum":["IMAGE","VIDEO","AUDIO","OTHER"]},"ExifResponseDto":{"type":"object","properties":{"id":{"type":"integer","nullable":true,"default":null,"format":"int64"},"fileSizeInByte":{"type":"integer","nullable":true,"default":null,"format":"int64"},"make":{"type":"string","nullable":true,"default":null},"model":{"type":"string","nullable":true,"default":null},"imageName":{"type":"string","nullable":true,"default":null},"exifImageWidth":{"type":"number","nullable":true,"default":null},"exifImageHeight":{"type":"number","nullable":true,"default":null},"orientation":{"type":"string","nullable":true,"default":null},"dateTimeOriginal":{"format":"date-time","type":"string","nullable":true,"default":null},"modifyDate":{"format":"date-time","type":"string","nullable":true,"default":null},"lensModel":{"type":"string","nullable":true,"default":null},"fNumber":{"type":"number","nullable":true,"default":null},"focalLength":{"type":"number","nullable":true,"default":null},"iso":{"type":"number","nullable":true,"default":null},"exposureTime":{"type":"number","nullable":true,"default":null},"latitude":{"type":"number","nullable":true,"default":null},"longitude":{"type":"number","nullable":true,"default":null},"city":{"type":"string","nullable":true,"default":null},"state":{"type":"string","nullable":true,"default":null},"country":{"type":"string","nullable":true,"default":null}}},"SmartInfoResponseDto":{"type":"object","properties":{"id":{"type":"string"},"tags":{"nullable":true,"type":"array","items":{"type":"string"}},"objects":{"nullable":true,"type":"array","items":{"type":"string"}}}},"AssetResponseDto":{"type":"object","properties":{"type":{"$ref":"#/components/schemas/AssetTypeEnum"},"id":{"type":"string"},"deviceAssetId":{"type":"string"},"ownerId":{"type":"string"},"deviceId":{"type":"string"},"originalPath":{"type":"string"},"resizePath":{"type":"string","nullable":true},"createdAt":{"type":"string"},"modifiedAt":{"type":"string"},"isFavorite":{"type":"boolean"},"mimeType":{"type":"string","nullable":true},"duration":{"type":"string"},"webpPath":{"type":"string","nullable":true},"encodedVideoPath":{"type":"string","nullable":true},"exifInfo":{"$ref":"#/components/schemas/ExifResponseDto"},"smartInfo":{"$ref":"#/components/schemas/SmartInfoResponseDto"},"livePhotoVideoId":{"type":"string","nullable":true}},"required":["type","id","deviceAssetId","ownerId","deviceId","originalPath","resizePath","createdAt","modifiedAt","isFavorite","mimeType","duration","webpPath","encodedVideoPath","livePhotoVideoId"]},"TimeGroupEnum":{"type":"string","enum":["day","month"]},"GetAssetCountByTimeBucketDto":{"type":"object","properties":{"timeGroup":{"$ref":"#/components/schemas/TimeGroupEnum"}},"required":["timeGroup"]},"AssetCountByTimeBucket":{"type":"object","properties":{"timeBucket":{"type":"string"},"count":{"type":"integer"}},"required":["timeBucket","count"]},"AssetCountByTimeBucketResponseDto":{"type":"object","properties":{"totalCount":{"type":"integer"},"buckets":{"type":"array","items":{"$ref":"#/components/schemas/AssetCountByTimeBucket"}}},"required":["totalCount","buckets"]},"AssetCountByUserIdResponseDto":{"type":"object","properties":{"audio":{"type":"integer","default":0},"photos":{"type":"integer","default":0},"videos":{"type":"integer","default":0},"other":{"type":"integer","default":0},"total":{"type":"integer","default":0}},"required":["audio","photos","videos","other","total"]},"GetAssetByTimeBucketDto":{"type":"object","properties":{"timeBucket":{"title":"Array of date time buckets","example":["2015-06-01T00:00:00.000Z","2016-02-01T00:00:00.000Z","2016-03-01T00:00:00.000Z"],"type":"array","items":{"type":"string"}}},"required":["timeBucket"]},"UpdateAssetDto":{"type":"object","properties":{"isFavorite":{"type":"boolean"}},"required":["isFavorite"]},"DeleteAssetDto":{"type":"object","properties":{"ids":{"title":"Array of asset IDs to delete","example":["bf973405-3f2a-48d2-a687-2ed4167164be","dd41870b-5d00-46d2-924e-1d8489a0aa0f","fad77c3f-deef-4e7e-9608-14c1aa4e559a"],"type":"array","items":{"type":"string"}}},"required":["ids"]},"DeleteAssetStatus":{"type":"string","enum":["SUCCESS","FAILED"]},"DeleteAssetResponseDto":{"type":"object","properties":{"status":{"$ref":"#/components/schemas/DeleteAssetStatus"},"id":{"type":"string"}},"required":["status","id"]},"CheckDuplicateAssetDto":{"type":"object","properties":{"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["deviceAssetId","deviceId"]},"CheckDuplicateAssetResponseDto":{"type":"object","properties":{"isExist":{"type":"boolean"},"id":{"type":"string"}},"required":["isExist"]},"CheckExistingAssetsDto":{"type":"object","properties":{"deviceAssetIds":{"type":"array","items":{"type":"string"}},"deviceId":{"type":"string"}},"required":["deviceAssetIds","deviceId"]},"CheckExistingAssetsResponseDto":{"type":"object","properties":{"existingIds":{"type":"array","items":{"type":"string"}}},"required":["existingIds"]},"LoginCredentialDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"}},"required":["email","password"]},"LoginResponseDto":{"type":"object","properties":{"accessToken":{"type":"string","readOnly":true},"userId":{"type":"string","readOnly":true},"userEmail":{"type":"string","readOnly":true},"firstName":{"type":"string","readOnly":true},"lastName":{"type":"string","readOnly":true},"profileImagePath":{"type":"string","readOnly":true},"isAdmin":{"type":"boolean","readOnly":true},"shouldChangePassword":{"type":"boolean","readOnly":true}},"required":["accessToken","userId","userEmail","firstName","lastName","profileImagePath","isAdmin","shouldChangePassword"]},"SignUpDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"Admin"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"AdminSignupResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"}},"required":["id","email","firstName","lastName","createdAt"]},"ValidateAccessTokenResponseDto":{"type":"object","properties":{"authStatus":{"type":"boolean"}},"required":["authStatus"]},"LogoutResponseDto":{"type":"object","properties":{"successful":{"type":"boolean","readOnly":true},"redirectUri":{"type":"string","readOnly":true}},"required":["successful","redirectUri"]},"OAuthConfigDto":{"type":"object","properties":{"redirectUri":{"type":"string"}},"required":["redirectUri"]},"OAuthConfigResponseDto":{"type":"object","properties":{"enabled":{"type":"boolean","readOnly":true},"url":{"type":"string","readOnly":true},"buttonText":{"type":"string","readOnly":true}},"required":["enabled"]},"OAuthCallbackDto":{"type":"object","properties":{"url":{"type":"string"}},"required":["url"]},"DeviceTypeEnum":{"type":"string","enum":["IOS","ANDROID","WEB"]},"CreateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"DeviceInfoResponseDto":{"type":"object","properties":{"id":{"type":"integer"},"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"userId":{"type":"string"},"deviceId":{"type":"string"},"createdAt":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["id","deviceType","userId","deviceId","createdAt","isAutoBackup"]},"UpdateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"ServerInfoResponseDto":{"type":"object","properties":{"diskSizeRaw":{"type":"integer","format":"int64"},"diskUseRaw":{"type":"integer","format":"int64"},"diskAvailableRaw":{"type":"integer","format":"int64"},"diskUsagePercentage":{"type":"number","format":"float"},"diskSize":{"type":"string"},"diskUse":{"type":"string"},"diskAvailable":{"type":"string"}},"required":["diskSizeRaw","diskUseRaw","diskAvailableRaw","diskUsagePercentage","diskSize","diskUse","diskAvailable"]},"ServerPingResponse":{"type":"object","properties":{"res":{"type":"string","readOnly":true,"example":"pong"}},"required":["res"]},"ServerVersionReponseDto":{"type":"object","properties":{"major":{"type":"integer"},"minor":{"type":"integer"},"patch":{"type":"integer"},"build":{"type":"integer"}},"required":["major","minor","patch","build"]},"UsageByUserDto":{"type":"object","properties":{"userId":{"type":"string"},"videos":{"type":"integer"},"photos":{"type":"integer"},"usageRaw":{"type":"integer","format":"int64"},"usage":{"type":"string"}},"required":["userId","videos","photos","usageRaw","usage"]},"ServerStatsResponseDto":{"type":"object","properties":{"photos":{"type":"integer"},"videos":{"type":"integer"},"objects":{"type":"integer"},"usageRaw":{"type":"integer","format":"int64"},"usage":{"type":"string"},"usageByUser":{"title":"Array of usage for each user","example":[{"photos":1,"videos":1,"diskUsageRaw":1}],"type":"array","items":{"$ref":"#/components/schemas/UsageByUserDto"}}},"required":["photos","videos","objects","usageRaw","usage","usageByUser"]},"AlbumCountResponseDto":{"type":"object","properties":{"owned":{"type":"integer"},"shared":{"type":"integer"},"sharing":{"type":"integer"}},"required":["owned","shared","sharing"]},"CreateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"sharedWithUserIds":{"type":"array","items":{"type":"string"}},"assetIds":{"type":"array","items":{"type":"string"}}},"required":["albumName"]},"AlbumResponseDto":{"type":"object","properties":{"assetCount":{"type":"integer"},"id":{"type":"string"},"ownerId":{"type":"string"},"albumName":{"type":"string"},"createdAt":{"type":"string"},"albumThumbnailAssetId":{"type":"string","nullable":true},"shared":{"type":"boolean"},"sharedUsers":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}},"assets":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}},"required":["assetCount","id","ownerId","albumName","createdAt","albumThumbnailAssetId","shared","sharedUsers","assets"]},"AddUsersDto":{"type":"object","properties":{"sharedUserIds":{"type":"array","items":{"type":"string"}}},"required":["sharedUserIds"]},"AddAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"AddAssetsResponseDto":{"type":"object","properties":{"successfullyAdded":{"type":"integer"},"alreadyInAlbum":{"type":"array","items":{"type":"string"}},"album":{"$ref":"#/components/schemas/AlbumResponseDto"}},"required":["successfullyAdded","alreadyInAlbum"]},"RemoveAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"UpdateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"albumThumbnailAssetId":{"type":"string"}}},"JobCounts":{"type":"object","properties":{"active":{"type":"integer"},"completed":{"type":"integer"},"failed":{"type":"integer"},"delayed":{"type":"integer"},"waiting":{"type":"integer"}},"required":["active","completed","failed","delayed","waiting"]},"AllJobStatusResponseDto":{"type":"object","properties":{"thumbnailGenerationQueueCount":{"$ref":"#/components/schemas/JobCounts"},"metadataExtractionQueueCount":{"$ref":"#/components/schemas/JobCounts"},"videoConversionQueueCount":{"$ref":"#/components/schemas/JobCounts"},"machineLearningQueueCount":{"$ref":"#/components/schemas/JobCounts"},"isThumbnailGenerationActive":{"type":"boolean"},"isMetadataExtractionActive":{"type":"boolean"},"isVideoConversionActive":{"type":"boolean"},"isMachineLearningActive":{"type":"boolean"}},"required":["thumbnailGenerationQueueCount","metadataExtractionQueueCount","videoConversionQueueCount","machineLearningQueueCount","isThumbnailGenerationActive","isMetadataExtractionActive","isVideoConversionActive","isMachineLearningActive"]},"JobId":{"type":"string","enum":["thumbnail-generation","metadata-extraction","video-conversion","machine-learning"]},"JobStatusResponseDto":{"type":"object","properties":{"isActive":{"type":"boolean"},"queueCount":{"type":"object"}},"required":["isActive","queueCount"]},"JobCommand":{"type":"string","enum":["start","stop"]},"JobCommandDto":{"type":"object","properties":{"command":{"$ref":"#/components/schemas/JobCommand"}},"required":["command"]},"SystemConfigKey":{"type":"string","enum":["ffmpeg_crf","ffmpeg_preset","ffmpeg_target_video_codec","ffmpeg_target_audio_codec","ffmpeg_target_scaling"]},"SystemConfigResponseItem":{"type":"object","properties":{"name":{"type":"string"},"key":{"$ref":"#/components/schemas/SystemConfigKey"},"value":{"type":"string"},"defaultValue":{"type":"string"}},"required":["name","key","value","defaultValue"]},"SystemConfigResponseDto":{"type":"object","properties":{"config":{"type":"array","items":{"$ref":"#/components/schemas/SystemConfigResponseItem"}}},"required":["config"]},"UpdateSystemConfigDto":{"type":"object","properties":{}}}}} \ No newline at end of file +{"openapi":"3.0.0","paths":{"/user":{"get":{"operationId":"getAllUsers","parameters":[{"name":"isAll","required":true,"in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}}}}}},"tags":["User"],"security":[{"bearer":[]}]},"post":{"operationId":"createUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]},"put":{"operationId":"updateUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/info/{userId}":{"get":{"operationId":"getUserById","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"]}},"/user/me":{"get":{"operationId":"getMyUserInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/count":{"get":{"operationId":"getUserCount","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserCountResponseDto"}}}}},"tags":["User"]}},"/user/{userId}":{"delete":{"operationId":"deleteUser","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/{userId}/restore":{"post":{"operationId":"restoreUser","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/profile-image":{"post":{"operationId":"createProfileImage","parameters":[],"requestBody":{"required":true,"description":"A new avatar for the user","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/CreateProfileImageDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProfileImageResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/profile-image/{userId}":{"get":{"operationId":"getProfileImage","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["User"]}},"/asset/upload":{"post":{"operationId":"uploadFile","parameters":[],"requestBody":{"required":true,"description":"Asset Upload Information","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/AssetFileUploadDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetFileUploadResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/download/{assetId}":{"get":{"operationId":"downloadFile","parameters":[{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}},{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/download-library":{"get":{"operationId":"downloadLibrary","parameters":[{"name":"skip","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/file/{assetId}":{"get":{"operationId":"serveFile","parameters":[{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}},{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/thumbnail/{assetId}":{"get":{"operationId":"getAssetThumbnail","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}},{"name":"format","required":false,"in":"query","schema":{"$ref":"#/components/schemas/ThumbnailFormat"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/curated-objects":{"get":{"operationId":"getCuratedObjects","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedObjectsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/curated-locations":{"get":{"operationId":"getCuratedLocations","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedLocationsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search-terms":{"get":{"operationId":"getAssetSearchTerms","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search":{"post":{"operationId":"searchAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchAssetDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/count-by-time-bucket":{"post":{"operationId":"getAssetCountByTimeBucket","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAssetCountByTimeBucketDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCountByTimeBucketResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/count-by-user-id":{"get":{"operationId":"getAssetCountByUserId","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCountByUserIdResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset":{"get":{"operationId":"getAllAssets","summary":"","description":"Get all AssetEntity belong to the user","parameters":[{"name":"if-none-match","in":"header","description":"ETag of data already cached on the client","required":false,"schema":{"type":"string"}}],"responses":{"200":{"headers":{"ETag":{"required":true,"schema":{"type":"string"}}},"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DeleteAssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/time-bucket":{"post":{"operationId":"getAssetByTimeBucket","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAssetByTimeBucketDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/{deviceId}":{"get":{"operationId":"getUserAssetsByDeviceId","summary":"","description":"Get all asset of a device that are in the database, ID only.","parameters":[{"name":"deviceId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/assetById/{assetId}":{"get":{"operationId":"getAssetById","summary":"","description":"Get a single asset's information","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]},"put":{"operationId":"updateAssetById","summary":"","description":"Update an asset","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/check":{"post":{"operationId":"checkDuplicateAsset","summary":"","description":"Check duplicated asset before uploading - for Web upload used","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/exist":{"post":{"operationId":"checkExistingAssets","summary":"","description":"Checks if multiple assets exist on the server and returns all existing - used by background backup","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckExistingAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckExistingAssetsResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/auth/login":{"post":{"operationId":"login","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginCredentialDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponseDto"}}}}},"tags":["Authentication"]}},"/auth/admin-sign-up":{"post":{"operationId":"adminSignUp","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignUpDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSignupResponseDto"}}}},"400":{"description":"The server already has an admin"}},"tags":["Authentication"]}},"/auth/validateToken":{"post":{"operationId":"validateAccessToken","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateAccessTokenResponseDto"}}}}},"tags":["Authentication"],"security":[{"bearer":[]}]}},"/auth/logout":{"post":{"operationId":"logout","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogoutResponseDto"}}}}},"tags":["Authentication"]}},"/oauth/config":{"post":{"operationId":"generateConfig","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthConfigDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthConfigResponseDto"}}}}},"tags":["OAuth"]}},"/oauth/callback":{"post":{"operationId":"callback","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthCallbackDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponseDto"}}}}},"tags":["OAuth"]}},"/device-info":{"post":{"operationId":"createDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDeviceInfoDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDeviceInfoDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]}},"/server-info":{"get":{"operationId":"getServerInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerInfoResponseDto"}}}}},"tags":["Server Info"]}},"/server-info/ping":{"get":{"operationId":"pingServer","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerPingResponse"}}}}},"tags":["Server Info"]}},"/server-info/version":{"get":{"operationId":"getServerVersion","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerVersionReponseDto"}}}}},"tags":["Server Info"]}},"/server-info/stats":{"get":{"operationId":"getStats","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerStatsResponseDto"}}}}},"tags":["Server Info"]}},"/album/count-by-user-id":{"get":{"operationId":"getAlbumCountByUserId","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumCountResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album":{"post":{"operationId":"createAlbum","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAlbumDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"get":{"operationId":"getAllAlbums","parameters":[{"name":"shared","required":false,"in":"query","schema":{"type":"boolean"}},{"name":"assetId","required":false,"in":"query","description":"Only returns albums that contain the asset\nIgnores the shared parameter\nundefined: get all albums","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/users":{"put":{"operationId":"addUsersToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddUsersDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/assets":{"put":{"operationId":"addAssetsToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddAssetsResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"removeAssetFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}":{"get":{"operationId":"getAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAlbumDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/user/{userId}":{"delete":{"operationId":"removeUserFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}},{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/download":{"get":{"operationId":"downloadArchive","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}},{"name":"skip","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/jobs":{"get":{"operationId":"getAllJobsStatus","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AllJobStatusResponseDto"}}}}},"tags":["Job"],"security":[{"bearer":[]}]}},"/jobs/{jobId}":{"get":{"operationId":"getJobStatus","parameters":[{"name":"jobId","required":true,"in":"path","schema":{"$ref":"#/components/schemas/JobId"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobStatusResponseDto"}}}}},"tags":["Job"],"security":[{"bearer":[]}]},"put":{"operationId":"sendJobCommand","parameters":[{"name":"jobId","required":true,"in":"path","schema":{"$ref":"#/components/schemas/JobId"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobCommandDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"number"}}}}},"tags":["Job"],"security":[{"bearer":[]}]}},"/system-config":{"get":{"operationId":"getConfig","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemConfigResponseDto"}}}}},"tags":["System Config"],"security":[{"bearer":[]}]},"put":{"operationId":"updateConfig","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSystemConfigDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemConfigResponseDto"}}}}},"tags":["System Config"],"security":[{"bearer":[]}]}}},"info":{"title":"Immich","description":"Immich API","version":"1.17.0","contact":{}},"tags":[],"servers":[{"url":"/api"}],"components":{"securitySchemes":{"bearer":{"scheme":"Bearer","bearerFormat":"JWT","type":"http","name":"JWT","description":"Enter JWT token","in":"header"}},"schemas":{"UserResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"},"profileImagePath":{"type":"string"},"shouldChangePassword":{"type":"boolean"},"isAdmin":{"type":"boolean"},"deletedAt":{"format":"date-time","type":"string"}},"required":["id","email","firstName","lastName","createdAt","profileImagePath","shouldChangePassword","isAdmin"]},"CreateUserDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"John"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"UserCountResponseDto":{"type":"object","properties":{"userCount":{"type":"integer"}},"required":["userCount"]},"UpdateUserDto":{"type":"object","properties":{"id":{"type":"string"},"password":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"isAdmin":{"type":"boolean"},"shouldChangePassword":{"type":"boolean"},"profileImagePath":{"type":"string"}},"required":["id"]},"CreateProfileImageDto":{"type":"object","properties":{"file":{"type":"string","format":"binary"}},"required":["file"]},"CreateProfileImageResponseDto":{"type":"object","properties":{"userId":{"type":"string"},"profileImagePath":{"type":"string"}},"required":["userId","profileImagePath"]},"AssetFileUploadDto":{"type":"object","properties":{"assetData":{"type":"string","format":"binary"}},"required":["assetData"]},"AssetFileUploadResponseDto":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]},"ThumbnailFormat":{"type":"string","enum":["JPEG","WEBP"]},"CuratedObjectsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","object","resizePath","deviceAssetId","deviceId"]},"CuratedLocationsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"city":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","city","resizePath","deviceAssetId","deviceId"]},"SearchAssetDto":{"type":"object","properties":{"searchTerm":{"type":"string"}},"required":["searchTerm"]},"AssetTypeEnum":{"type":"string","enum":["IMAGE","VIDEO","AUDIO","OTHER"]},"ExifResponseDto":{"type":"object","properties":{"id":{"type":"integer","nullable":true,"default":null,"format":"int64"},"fileSizeInByte":{"type":"integer","nullable":true,"default":null,"format":"int64"},"make":{"type":"string","nullable":true,"default":null},"model":{"type":"string","nullable":true,"default":null},"imageName":{"type":"string","nullable":true,"default":null},"exifImageWidth":{"type":"number","nullable":true,"default":null},"exifImageHeight":{"type":"number","nullable":true,"default":null},"orientation":{"type":"string","nullable":true,"default":null},"dateTimeOriginal":{"format":"date-time","type":"string","nullable":true,"default":null},"modifyDate":{"format":"date-time","type":"string","nullable":true,"default":null},"lensModel":{"type":"string","nullable":true,"default":null},"fNumber":{"type":"number","nullable":true,"default":null},"focalLength":{"type":"number","nullable":true,"default":null},"iso":{"type":"number","nullable":true,"default":null},"exposureTime":{"type":"number","nullable":true,"default":null},"latitude":{"type":"number","nullable":true,"default":null},"longitude":{"type":"number","nullable":true,"default":null},"city":{"type":"string","nullable":true,"default":null},"state":{"type":"string","nullable":true,"default":null},"country":{"type":"string","nullable":true,"default":null}}},"SmartInfoResponseDto":{"type":"object","properties":{"id":{"type":"string"},"tags":{"nullable":true,"type":"array","items":{"type":"string"}},"objects":{"nullable":true,"type":"array","items":{"type":"string"}}}},"AssetResponseDto":{"type":"object","properties":{"type":{"$ref":"#/components/schemas/AssetTypeEnum"},"id":{"type":"string"},"deviceAssetId":{"type":"string"},"ownerId":{"type":"string"},"deviceId":{"type":"string"},"originalPath":{"type":"string"},"resizePath":{"type":"string","nullable":true},"createdAt":{"type":"string"},"modifiedAt":{"type":"string"},"isFavorite":{"type":"boolean"},"mimeType":{"type":"string","nullable":true},"duration":{"type":"string"},"webpPath":{"type":"string","nullable":true},"encodedVideoPath":{"type":"string","nullable":true},"exifInfo":{"$ref":"#/components/schemas/ExifResponseDto"},"smartInfo":{"$ref":"#/components/schemas/SmartInfoResponseDto"},"livePhotoVideoId":{"type":"string","nullable":true}},"required":["type","id","deviceAssetId","ownerId","deviceId","originalPath","resizePath","createdAt","modifiedAt","isFavorite","mimeType","duration","webpPath"]},"TimeGroupEnum":{"type":"string","enum":["day","month"]},"GetAssetCountByTimeBucketDto":{"type":"object","properties":{"timeGroup":{"$ref":"#/components/schemas/TimeGroupEnum"}},"required":["timeGroup"]},"AssetCountByTimeBucket":{"type":"object","properties":{"timeBucket":{"type":"string"},"count":{"type":"integer"}},"required":["timeBucket","count"]},"AssetCountByTimeBucketResponseDto":{"type":"object","properties":{"totalCount":{"type":"integer"},"buckets":{"type":"array","items":{"$ref":"#/components/schemas/AssetCountByTimeBucket"}}},"required":["totalCount","buckets"]},"AssetCountByUserIdResponseDto":{"type":"object","properties":{"audio":{"type":"integer","default":0},"photos":{"type":"integer","default":0},"videos":{"type":"integer","default":0},"other":{"type":"integer","default":0},"total":{"type":"integer","default":0}},"required":["audio","photos","videos","other","total"]},"GetAssetByTimeBucketDto":{"type":"object","properties":{"timeBucket":{"title":"Array of date time buckets","example":["2015-06-01T00:00:00.000Z","2016-02-01T00:00:00.000Z","2016-03-01T00:00:00.000Z"],"type":"array","items":{"type":"string"}}},"required":["timeBucket"]},"UpdateAssetDto":{"type":"object","properties":{"isFavorite":{"type":"boolean"}},"required":["isFavorite"]},"DeleteAssetDto":{"type":"object","properties":{"ids":{"title":"Array of asset IDs to delete","example":["bf973405-3f2a-48d2-a687-2ed4167164be","dd41870b-5d00-46d2-924e-1d8489a0aa0f","fad77c3f-deef-4e7e-9608-14c1aa4e559a"],"type":"array","items":{"type":"string"}}},"required":["ids"]},"DeleteAssetStatus":{"type":"string","enum":["SUCCESS","FAILED"]},"DeleteAssetResponseDto":{"type":"object","properties":{"status":{"$ref":"#/components/schemas/DeleteAssetStatus"},"id":{"type":"string"}},"required":["status","id"]},"CheckDuplicateAssetDto":{"type":"object","properties":{"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["deviceAssetId","deviceId"]},"CheckDuplicateAssetResponseDto":{"type":"object","properties":{"isExist":{"type":"boolean"},"id":{"type":"string"}},"required":["isExist"]},"CheckExistingAssetsDto":{"type":"object","properties":{"deviceAssetIds":{"type":"array","items":{"type":"string"}},"deviceId":{"type":"string"}},"required":["deviceAssetIds","deviceId"]},"CheckExistingAssetsResponseDto":{"type":"object","properties":{"existingIds":{"type":"array","items":{"type":"string"}}},"required":["existingIds"]},"LoginCredentialDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"}},"required":["email","password"]},"LoginResponseDto":{"type":"object","properties":{"accessToken":{"type":"string","readOnly":true},"userId":{"type":"string","readOnly":true},"userEmail":{"type":"string","readOnly":true},"firstName":{"type":"string","readOnly":true},"lastName":{"type":"string","readOnly":true},"profileImagePath":{"type":"string","readOnly":true},"isAdmin":{"type":"boolean","readOnly":true},"shouldChangePassword":{"type":"boolean","readOnly":true}},"required":["accessToken","userId","userEmail","firstName","lastName","profileImagePath","isAdmin","shouldChangePassword"]},"SignUpDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"Admin"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"AdminSignupResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"}},"required":["id","email","firstName","lastName","createdAt"]},"ValidateAccessTokenResponseDto":{"type":"object","properties":{"authStatus":{"type":"boolean"}},"required":["authStatus"]},"LogoutResponseDto":{"type":"object","properties":{"successful":{"type":"boolean","readOnly":true},"redirectUri":{"type":"string","readOnly":true}},"required":["successful","redirectUri"]},"OAuthConfigDto":{"type":"object","properties":{"redirectUri":{"type":"string"}},"required":["redirectUri"]},"OAuthConfigResponseDto":{"type":"object","properties":{"enabled":{"type":"boolean","readOnly":true},"url":{"type":"string","readOnly":true},"buttonText":{"type":"string","readOnly":true}},"required":["enabled"]},"OAuthCallbackDto":{"type":"object","properties":{"url":{"type":"string"}},"required":["url"]},"DeviceTypeEnum":{"type":"string","enum":["IOS","ANDROID","WEB"]},"CreateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"DeviceInfoResponseDto":{"type":"object","properties":{"id":{"type":"integer"},"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"userId":{"type":"string"},"deviceId":{"type":"string"},"createdAt":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["id","deviceType","userId","deviceId","createdAt","isAutoBackup"]},"UpdateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"ServerInfoResponseDto":{"type":"object","properties":{"diskSizeRaw":{"type":"integer","format":"int64"},"diskUseRaw":{"type":"integer","format":"int64"},"diskAvailableRaw":{"type":"integer","format":"int64"},"diskUsagePercentage":{"type":"number","format":"float"},"diskSize":{"type":"string"},"diskUse":{"type":"string"},"diskAvailable":{"type":"string"}},"required":["diskSizeRaw","diskUseRaw","diskAvailableRaw","diskUsagePercentage","diskSize","diskUse","diskAvailable"]},"ServerPingResponse":{"type":"object","properties":{"res":{"type":"string","readOnly":true,"example":"pong"}},"required":["res"]},"ServerVersionReponseDto":{"type":"object","properties":{"major":{"type":"integer"},"minor":{"type":"integer"},"patch":{"type":"integer"},"build":{"type":"integer"}},"required":["major","minor","patch","build"]},"UsageByUserDto":{"type":"object","properties":{"userId":{"type":"string"},"videos":{"type":"integer"},"photos":{"type":"integer"},"usageRaw":{"type":"integer","format":"int64"},"usage":{"type":"string"}},"required":["userId","videos","photos","usageRaw","usage"]},"ServerStatsResponseDto":{"type":"object","properties":{"photos":{"type":"integer"},"videos":{"type":"integer"},"objects":{"type":"integer"},"usageRaw":{"type":"integer","format":"int64"},"usage":{"type":"string"},"usageByUser":{"title":"Array of usage for each user","example":[{"photos":1,"videos":1,"diskUsageRaw":1}],"type":"array","items":{"$ref":"#/components/schemas/UsageByUserDto"}}},"required":["photos","videos","objects","usageRaw","usage","usageByUser"]},"AlbumCountResponseDto":{"type":"object","properties":{"owned":{"type":"integer"},"shared":{"type":"integer"},"sharing":{"type":"integer"}},"required":["owned","shared","sharing"]},"CreateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"sharedWithUserIds":{"type":"array","items":{"type":"string"}},"assetIds":{"type":"array","items":{"type":"string"}}},"required":["albumName"]},"AlbumResponseDto":{"type":"object","properties":{"assetCount":{"type":"integer"},"id":{"type":"string"},"ownerId":{"type":"string"},"albumName":{"type":"string"},"createdAt":{"type":"string"},"albumThumbnailAssetId":{"type":"string","nullable":true},"shared":{"type":"boolean"},"sharedUsers":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}},"assets":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}},"required":["assetCount","id","ownerId","albumName","createdAt","albumThumbnailAssetId","shared","sharedUsers","assets"]},"AddUsersDto":{"type":"object","properties":{"sharedUserIds":{"type":"array","items":{"type":"string"}}},"required":["sharedUserIds"]},"AddAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"AddAssetsResponseDto":{"type":"object","properties":{"successfullyAdded":{"type":"integer"},"alreadyInAlbum":{"type":"array","items":{"type":"string"}},"album":{"$ref":"#/components/schemas/AlbumResponseDto"}},"required":["successfullyAdded","alreadyInAlbum"]},"RemoveAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"UpdateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"albumThumbnailAssetId":{"type":"string"}}},"JobCounts":{"type":"object","properties":{"active":{"type":"integer"},"completed":{"type":"integer"},"failed":{"type":"integer"},"delayed":{"type":"integer"},"waiting":{"type":"integer"}},"required":["active","completed","failed","delayed","waiting"]},"AllJobStatusResponseDto":{"type":"object","properties":{"thumbnailGenerationQueueCount":{"$ref":"#/components/schemas/JobCounts"},"metadataExtractionQueueCount":{"$ref":"#/components/schemas/JobCounts"},"videoConversionQueueCount":{"$ref":"#/components/schemas/JobCounts"},"machineLearningQueueCount":{"$ref":"#/components/schemas/JobCounts"},"isThumbnailGenerationActive":{"type":"boolean"},"isMetadataExtractionActive":{"type":"boolean"},"isVideoConversionActive":{"type":"boolean"},"isMachineLearningActive":{"type":"boolean"}},"required":["thumbnailGenerationQueueCount","metadataExtractionQueueCount","videoConversionQueueCount","machineLearningQueueCount","isThumbnailGenerationActive","isMetadataExtractionActive","isVideoConversionActive","isMachineLearningActive"]},"JobId":{"type":"string","enum":["thumbnail-generation","metadata-extraction","video-conversion","machine-learning"]},"JobStatusResponseDto":{"type":"object","properties":{"isActive":{"type":"boolean"},"queueCount":{"type":"object"}},"required":["isActive","queueCount"]},"JobCommand":{"type":"string","enum":["start","stop"]},"JobCommandDto":{"type":"object","properties":{"command":{"$ref":"#/components/schemas/JobCommand"}},"required":["command"]},"SystemConfigKey":{"type":"string","enum":["ffmpeg_crf","ffmpeg_preset","ffmpeg_target_video_codec","ffmpeg_target_audio_codec","ffmpeg_target_scaling"]},"SystemConfigResponseItem":{"type":"object","properties":{"name":{"type":"string"},"key":{"$ref":"#/components/schemas/SystemConfigKey"},"value":{"type":"string"},"defaultValue":{"type":"string"}},"required":["name","key","value","defaultValue"]},"SystemConfigResponseDto":{"type":"object","properties":{"config":{"type":"array","items":{"$ref":"#/components/schemas/SystemConfigResponseItem"}}},"required":["config"]},"UpdateSystemConfigDto":{"type":"object","properties":{}}}}} \ No newline at end of file diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 39bd30a2fe..233800bdfe 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -427,7 +427,7 @@ export interface AssetResponseDto { * @type {string} * @memberof AssetResponseDto */ - 'encodedVideoPath': string | null; + 'encodedVideoPath'?: string | null; /** * * @type {ExifResponseDto} @@ -445,7 +445,7 @@ export interface AssetResponseDto { * @type {string} * @memberof AssetResponseDto */ - 'livePhotoVideoId': string | null; + 'livePhotoVideoId'?: string | null; } /** * @@ -1729,7 +1729,7 @@ export interface UserResponseDto { * @type {string} * @memberof UserResponseDto */ - 'deletedAt': string | null; + 'deletedAt'?: string; } /** * @@ -2788,10 +2788,11 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration /** * Get all AssetEntity belong to the user * @summary + * @param {string} [ifNoneMatch] ETag of data already cached on the client * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getAllAssets: async (options: AxiosRequestConfig = {}): Promise => { + getAllAssets: async (ifNoneMatch?: string, options: AxiosRequestConfig = {}): Promise => { const localVarPath = `/asset`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -2808,6 +2809,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration // http bearer authentication required await setBearerAuthToObject(localVarHeaderParameter, configuration) + if (ifNoneMatch !== undefined && ifNoneMatch !== null) { + localVarHeaderParameter['if-none-match'] = String(ifNoneMatch); + } + setSearchParams(localVarUrlObj, localVarQueryParameter); @@ -3388,11 +3393,12 @@ export const AssetApiFp = function(configuration?: Configuration) { /** * Get all AssetEntity belong to the user * @summary + * @param {string} [ifNoneMatch] ETag of data already cached on the client * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getAllAssets(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(options); + async getAllAssets(ifNoneMatch?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(ifNoneMatch, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -3590,11 +3596,12 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath /** * Get all AssetEntity belong to the user * @summary + * @param {string} [ifNoneMatch] ETag of data already cached on the client * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getAllAssets(options?: any): AxiosPromise> { - return localVarFp.getAllAssets(options).then((request) => request(axios, basePath)); + getAllAssets(ifNoneMatch?: string, options?: any): AxiosPromise> { + return localVarFp.getAllAssets(ifNoneMatch, options).then((request) => request(axios, basePath)); }, /** * Get a single asset\'s information @@ -3788,12 +3795,13 @@ export class AssetApi extends BaseAPI { /** * Get all AssetEntity belong to the user * @summary + * @param {string} [ifNoneMatch] ETag of data already cached on the client * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof AssetApi */ - public getAllAssets(options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).getAllAssets(options).then((request) => request(this.axios, this.basePath)); + public getAllAssets(ifNoneMatch?: string, options?: AxiosRequestConfig) { + return AssetApiFp(this.configuration).getAllAssets(ifNoneMatch, options).then((request) => request(this.axios, this.basePath)); } /** From 614743c8f42ffaf1accecbce50c844ff663f29b6 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 26 Nov 2022 15:02:23 -0600 Subject: [PATCH 21/30] fix(server): Prevent delete admin user (#1023) --- server/apps/immich/src/api-v1/user/user.service.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/apps/immich/src/api-v1/user/user.service.ts b/server/apps/immich/src/api-v1/user/user.service.ts index 311561856f..6fe2aafefd 100644 --- a/server/apps/immich/src/api-v1/user/user.service.ts +++ b/server/apps/immich/src/api-v1/user/user.service.ts @@ -119,6 +119,11 @@ export class UserService { if (!user) { throw new BadRequestException('User not found'); } + + if (user.isAdmin) { + throw new BadRequestException('Cannot delete admin user'); + } + try { const deletedUser = await this.userRepository.delete(user); return mapUser(deletedUser); From fb3b36a56967590b72f17c2299509b374fd0cdba Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Sat, 26 Nov 2022 15:09:06 -0600 Subject: [PATCH 22/30] Added test for user.service --- .../immich/src/api-v1/user/user.service.spec.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/server/apps/immich/src/api-v1/user/user.service.spec.ts b/server/apps/immich/src/api-v1/user/user.service.spec.ts index abed003833..9962752cae 100644 --- a/server/apps/immich/src/api-v1/user/user.service.spec.ts +++ b/server/apps/immich/src/api-v1/user/user.service.spec.ts @@ -1,5 +1,5 @@ import { UserEntity } from '@app/database/entities/user.entity'; -import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { BadRequestException, NotFoundException, UnauthorizedException } from '@nestjs/common'; import { newUserRepositoryMock } from '../../../test/test-utils'; import { AuthUserDto } from '../../decorators/auth-user.decorator'; import { IUserRepository } from './user-repository'; @@ -127,5 +127,16 @@ describe('UserService', () => { }); expect(result).rejects.toBeInstanceOf(NotFoundException); }); + + it('cannot delete admin user', () => { + const requestor = adminAuthUser; + + userRepositoryMock.get.mockImplementationOnce(() => Promise.resolve(adminUser)); + userRepositoryMock.get.mockImplementationOnce(() => Promise.resolve(adminUser)); + + const result = sui.deleteUser(requestor, adminAuthUser.id); + + expect(result).rejects.toBeInstanceOf(BadRequestException); + }); }); }); From 024177515de9164d4209b436a02c3beab58bd3af Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 27 Nov 2022 14:34:19 -0600 Subject: [PATCH 23/30] feat(mobile) Add in app logging to show app's log information (#1014) --- mobile/assets/i18n/en-US.json | 1 + mobile/fonts/Inconsolata-Regular.ttf | Bin 0 -> 97864 bytes mobile/lib/constants/hive_box.dart | 3 + mobile/lib/main.dart | 7 + .../background.service.dart | 1 - .../backup/providers/backup.provider.dart | 52 +++--- .../ui/profile_drawer/profile_drawer.dart | 29 +++- .../lib/modules/login/views/login_page.dart | 55 ++++++- mobile/lib/routing/router.dart | 31 ++-- mobile/lib/routing/router.gr.dart | 19 ++- .../models/immich_logger_message.model.dart | 34 ++++ .../models/immich_logger_message.model.g.dart | Bin 0 -> 1478 bytes .../lib/shared/providers/asset.provider.dart | 20 +-- .../providers/release_info.provider.dart | 6 +- .../shared/providers/websocket.provider.dart | 47 +++--- .../services/immich_logger.service.dart | 87 ++++++++++ mobile/lib/shared/views/app_log_page.dart | 153 ++++++++++++++++++ mobile/pubspec.lock | 20 +-- mobile/pubspec.yaml | 5 +- .../src/api-v1/user/user.service.spec.ts | 2 +- 20 files changed, 486 insertions(+), 86 deletions(-) create mode 100644 mobile/fonts/Inconsolata-Regular.ttf create mode 100644 mobile/lib/shared/models/immich_logger_message.model.dart create mode 100644 mobile/lib/shared/models/immich_logger_message.model.g.dart create mode 100644 mobile/lib/shared/services/immich_logger.service.dart create mode 100644 mobile/lib/shared/views/app_log_page.dart diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index fd3dadcadd..0adacb6494 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -120,6 +120,7 @@ "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date", "profile_drawer_settings": "Settings", "profile_drawer_sign_out": "Sign Out", + "profile_drawer_app_logs": "Logs", "search_bar_hint": "Search your photos", "search_page_no_objects": "No Objects Info Available", "search_page_no_places": "No Places Info Available", diff --git a/mobile/fonts/Inconsolata-Regular.ttf b/mobile/fonts/Inconsolata-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..0d879bf3a474dbd6989e73565cae9bfd752cef12 GIT binary patch literal 97864 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 literal 0 HcmV?d00001 diff --git a/mobile/lib/constants/hive_box.dart b/mobile/lib/constants/hive_box.dart index 9db1755053..704be3586e 100644 --- a/mobile/lib/constants/hive_box.dart +++ b/mobile/lib/constants/hive_box.dart @@ -30,3 +30,6 @@ const String backupRequireCharging = "immichBackupRequireCharging"; // Key 3 // Duplicate asset const String duplicatedAssetsBox = "immichDuplicatedAssetsBox"; // Box const String duplicatedAssetsKey = "immichDuplicatedAssetsKey"; // Key 1 + +// In app logger +const String immichLoggerBox = "immichInAppLogger"; // Box \ No newline at end of file diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index d1148df360..811dca7cff 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -16,11 +16,13 @@ import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.d import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/tab_navigation_observer.dart'; +import 'package:immich_mobile/shared/models/immich_logger_message.model.dart'; import 'package:immich_mobile/shared/providers/app_state.provider.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart'; import 'package:immich_mobile/shared/providers/release_info.provider.dart'; import 'package:immich_mobile/shared/providers/server_info.provider.dart'; import 'package:immich_mobile/shared/providers/websocket.provider.dart'; +import 'package:immich_mobile/shared/services/immich_logger.service.dart'; import 'package:immich_mobile/shared/views/immich_loading_overlay.dart'; import 'package:immich_mobile/shared/views/version_announcement_overlay.dart'; import 'package:immich_mobile/utils/immich_app_theme.dart'; @@ -31,8 +33,10 @@ void main() async { Hive.registerAdapter(HiveSavedLoginInfoAdapter()); Hive.registerAdapter(HiveBackupAlbumsAdapter()); Hive.registerAdapter(HiveDuplicatedAssetsAdapter()); + Hive.registerAdapter(ImmichLoggerMessageAdapter()); await Future.wait([ + Hive.openBox(immichLoggerBox), Hive.openBox(userInfoBox), Hive.openBox(hiveLoginInfoBox), Hive.openBox(hiveGithubReleaseInfoBox), @@ -58,6 +62,9 @@ void main() async { } } + // Initialize Immich Logger Service + ImmichLogger().init(); + runApp( EasyLocalization( supportedLocales: locales, diff --git a/mobile/lib/modules/backup/background_service/background.service.dart b/mobile/lib/modules/backup/background_service/background.service.dart index 69000c3ba5..8b48934917 100644 --- a/mobile/lib/modules/backup/background_service/background.service.dart +++ b/mobile/lib/modules/backup/background_service/background.service.dart @@ -349,7 +349,6 @@ class BackgroundService { Hive.openBox(duplicatedAssetsBox), Hive.openBox(hiveBackupInfoBox), ]); - ApiService apiService = ApiService(); apiService.setEndpoint(Hive.box(userInfoBox).get(serverEndpointKey)); apiService.setAccessToken(Hive.box(userInfoBox).get(accessTokenKey)); diff --git a/mobile/lib/modules/backup/providers/backup.provider.dart b/mobile/lib/modules/backup/providers/backup.provider.dart index ddf58fea84..65f0eba624 100644 --- a/mobile/lib/modules/backup/providers/backup.provider.dart +++ b/mobile/lib/modules/backup/providers/backup.provider.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:cancellation_token_http/http.dart'; -import 'package:flutter/foundation.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/hive_box.dart'; @@ -18,6 +17,7 @@ import 'package:immich_mobile/modules/login/models/authentication_state.model.da import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/shared/providers/app_state.provider.dart'; import 'package:immich_mobile/shared/services/server_info.service.dart'; +import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -62,6 +62,7 @@ class BackupNotifier extends StateNotifier { getBackupInfo(); } + final log = Logger('BackupNotifier'); final BackupService _backupService; final ServerInfoService _serverInfoService; final AuthenticationState _authState; @@ -218,13 +219,16 @@ class BackupNotifier extends StateNotifier { ); if (backupAlbumInfo == null) { - debugPrint("[ERROR] getting Hive backup album infomation"); + log.severe( + "backupAlbumInfo == null", + "Failed to get Hive backup album information", + ); return; } // First time backup - set isAll album is the default one for backup. if (backupAlbumInfo.selectedAlbumIds.isEmpty) { - debugPrint("First time backup setup recent album as default"); + log.info("First time backup; setup 'Recent(s)' album as default"); // Get album that contains all assets var list = await PhotoManager.getAssetPathList( @@ -286,8 +290,8 @@ class BackupNotifier extends StateNotifier { selectedBackupAlbums: selectedAlbums, excludedBackupAlbums: excludedAlbums, ); - } catch (e) { - debugPrint("[ERROR] Failed to generate album from id $e"); + } catch (e, stackTrace) { + log.severe("Failed to generate album from id", e, stackTrace); } } @@ -338,7 +342,7 @@ class BackupNotifier extends StateNotifier { ); if (allUniqueAssets.isEmpty) { - debugPrint("No Asset On Device"); + log.info("Not found albums or assets on the device to backup"); state = state.copyWith( backupProgress: BackUpProgressEnum.idle, allAssetsInDatabase: allAssetsInDatabase, @@ -412,7 +416,7 @@ class BackupNotifier extends StateNotifier { await PhotoManager.clearFileCache(); if (state.allUniqueAssets.isEmpty) { - debugPrint("No Asset On Device - Abort Backup Process"); + log.info("No Asset On Device - Abort Backup Process"); state = state.copyWith(backupProgress: BackUpProgressEnum.idle); return; } @@ -530,7 +534,7 @@ class BackupNotifier extends StateNotifier { // User has been logged out return if (accessKey == null || !_authState.isAuthenticated) { - debugPrint("[resumeBackup] not authenticated - abort"); + log.info("[_resumeBackup] not authenticated - abort"); return; } @@ -539,17 +543,17 @@ class BackupNotifier extends StateNotifier { _authState.deviceInfo.isAutoBackup) { // check if backup is alreayd in process - then return if (state.backupProgress == BackUpProgressEnum.inProgress) { - debugPrint("[resumeBackup] Backup is already in progress - abort"); + log.info("[_resumeBackup] Backup is already in progress - abort"); return; } if (state.backupProgress == BackUpProgressEnum.inBackground) { - debugPrint("[resumeBackup] Background backup is running - abort"); + log.info("[_resumeBackup] Background backup is running - abort"); return; } // Run backup - debugPrint("[resumeBackup] Start back up"); + log.info("[_resumeBackup] Start back up"); await startBackupProcess(); } @@ -565,7 +569,7 @@ class BackupNotifier extends StateNotifier { state = state.copyWith(backupProgress: BackUpProgressEnum.inBackground); final bool hasLock = await _backgroundService.acquireLock(); if (!hasLock) { - debugPrint("WARNING [resumeBackup] failed to acquireLock"); + log.warning("WARNING [resumeBackup] failed to acquireLock"); return; } await Future.wait([ @@ -612,7 +616,11 @@ class BackupNotifier extends StateNotifier { AvailableAlbum a = albums.firstWhere((e) => e.id == ids[i]); result.add(a.copyWith(lastBackup: times[i])); } on StateError { - debugPrint("[_updateAlbumBackupTime] failed to find album in state"); + log.severe( + "[_updateAlbumBackupTime] failed to find album in state", + "State Error", + StackTrace.current, + ); } } return result; @@ -631,21 +639,29 @@ class BackupNotifier extends StateNotifier { await Hive.box(hiveBackupInfoBox).close(); } } catch (error) { - debugPrint("[_notifyBackgroundServiceCanRun] failed to close box"); + log.info("[_notifyBackgroundServiceCanRun] failed to close box"); } try { if (Hive.isBoxOpen(duplicatedAssetsBox)) { await Hive.box(duplicatedAssetsBox).close(); } - } catch (error) { - debugPrint("[_notifyBackgroundServiceCanRun] failed to close box"); + } catch (error, stackTrace) { + log.severe( + "[_notifyBackgroundServiceCanRun] failed to close box", + error, + stackTrace, + ); } try { if (Hive.isBoxOpen(backgroundBackupInfoBox)) { await Hive.box(backgroundBackupInfoBox).close(); } - } catch (error) { - debugPrint("[_notifyBackgroundServiceCanRun] failed to close box"); + } catch (error, stackTrace) { + log.severe( + "[_notifyBackgroundServiceCanRun] failed to close box", + error, + stackTrace, + ); } _backgroundService.releaseLock(); } diff --git a/mobile/lib/modules/home/ui/profile_drawer/profile_drawer.dart b/mobile/lib/modules/home/ui/profile_drawer/profile_drawer.dart index d984a70af6..04e7e648e8 100644 --- a/mobile/lib/modules/home/ui/profile_drawer/profile_drawer.dart +++ b/mobile/lib/modules/home/ui/profile_drawer/profile_drawer.dart @@ -2,12 +2,12 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer_header.dart'; import 'package:immich_mobile/modules/home/ui/profile_drawer/server_info_box.dart'; +import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart'; -import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; -import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/shared/providers/websocket.provider.dart'; class ProfileDrawer extends HookConsumerWidget { @@ -70,6 +70,30 @@ class ProfileDrawer extends HookConsumerWidget { ); } + buildAppLogButton() { + return ListTile( + horizontalTitleGap: 0, + leading: SizedBox( + height: double.infinity, + child: Icon( + Icons.assignment_outlined, + color: Theme.of(context).textTheme.labelMedium?.color, + size: 20, + ), + ), + title: Text( + "profile_drawer_app_logs", + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith(fontWeight: FontWeight.bold), + ).tr(), + onTap: () { + AutoRouter.of(context).push(const AppLogRoute()); + }, + ); + } + return Drawer( child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -80,6 +104,7 @@ class ProfileDrawer extends HookConsumerWidget { children: [ const ProfileDrawerHeader(), buildSettingButton(), + buildAppLogButton(), buildSignoutButton(), ], ), diff --git a/mobile/lib/modules/login/views/login_page.dart b/mobile/lib/modules/login/views/login_page.dart index 42c02a6eac..98778736e3 100644 --- a/mobile/lib/modules/login/views/login_page.dart +++ b/mobile/lib/modules/login/views/login_page.dart @@ -1,14 +1,65 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/login/ui/login_form.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:package_info_plus/package_info_plus.dart'; class LoginPage extends HookConsumerWidget { const LoginPage({Key? key}) : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { - return const Scaffold( - body: LoginForm(), + final appVersion = useState('0.0.0'); + + getAppInfo() async { + PackageInfo packageInfo = await PackageInfo.fromPlatform(); + appVersion.value = packageInfo.version; + } + + useEffect( + () { + getAppInfo(); + return null; + }, + ); + + return Scaffold( + body: const LoginForm(), + bottomNavigationBar: Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: SizedBox( + height: 50, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'v${appVersion.value}', + style: const TextStyle( + color: Colors.grey, + fontWeight: FontWeight.bold, + fontFamily: "Inconsolata", + ), + ), + const Text(' '), + GestureDetector( + child: Text( + 'Logs', + style: TextStyle( + color: Theme.of(context).primaryColor, + fontWeight: FontWeight.bold, + fontFamily: "Inconsolata", + ), + ), + onTap: () { + AutoRouter.of(context).push(const AppLogRoute()); + }, + ), + ], + ), + ), + ), ); } } diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 3a00ae8c81..313a53e5fb 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -1,33 +1,34 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/album/views/library_page.dart'; -import 'package:immich_mobile/modules/asset_viewer/views/gallery_viewer.dart'; -import 'package:immich_mobile/modules/backup/views/album_preview_page.dart'; -import 'package:immich_mobile/modules/backup/views/backup_album_selection_page.dart'; -import 'package:immich_mobile/modules/backup/views/failed_backup_status_page.dart'; -import 'package:immich_mobile/modules/login/views/change_password_page.dart'; -import 'package:immich_mobile/modules/login/views/login_page.dart'; -import 'package:immich_mobile/modules/home/views/home_page.dart'; -import 'package:immich_mobile/modules/search/views/search_page.dart'; -import 'package:immich_mobile/modules/search/views/search_result_page.dart'; import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart'; import 'package:immich_mobile/modules/album/views/album_viewer_page.dart'; import 'package:immich_mobile/modules/album/views/asset_selection_page.dart'; import 'package:immich_mobile/modules/album/views/create_album_page.dart'; +import 'package:immich_mobile/modules/album/views/library_page.dart'; import 'package:immich_mobile/modules/album/views/select_additional_user_for_sharing_page.dart'; import 'package:immich_mobile/modules/album/views/select_user_for_sharing_page.dart'; import 'package:immich_mobile/modules/album/views/sharing_page.dart'; +import 'package:immich_mobile/modules/asset_viewer/views/gallery_viewer.dart'; +import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart'; +import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart'; +import 'package:immich_mobile/modules/backup/views/album_preview_page.dart'; +import 'package:immich_mobile/modules/backup/views/backup_album_selection_page.dart'; +import 'package:immich_mobile/modules/backup/views/backup_controller_page.dart'; +import 'package:immich_mobile/modules/backup/views/failed_backup_status_page.dart'; +import 'package:immich_mobile/modules/home/views/home_page.dart'; +import 'package:immich_mobile/modules/login/views/change_password_page.dart'; +import 'package:immich_mobile/modules/login/views/login_page.dart'; +import 'package:immich_mobile/modules/search/views/search_page.dart'; +import 'package:immich_mobile/modules/search/views/search_result_page.dart'; import 'package:immich_mobile/modules/settings/views/settings_page.dart'; import 'package:immich_mobile/routing/auth_guard.dart'; -import 'package:immich_mobile/modules/backup/views/backup_controller_page.dart'; -import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/providers/api.provider.dart'; import 'package:immich_mobile/shared/services/api.service.dart'; +import 'package:immich_mobile/shared/views/app_log_page.dart'; import 'package:immich_mobile/shared/views/splash_screen.dart'; import 'package:immich_mobile/shared/views/tab_controller_page.dart'; -import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart'; import 'package:openapi/api.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -80,6 +81,10 @@ part 'router.gr.dart'; transitionsBuilder: TransitionsBuilders.slideBottom, ), AutoRoute(page: SettingsPage, guards: [AuthGuard]), + CustomRoute( + page: AppLogPage, + transitionsBuilder: TransitionsBuilders.slideBottom, + ), ], ) class AppRouter extends _$AppRouter { diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index d280114915..bd266c00cd 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -142,6 +142,14 @@ class _$AppRouter extends RootStackRouter { return MaterialPageX( routeData: routeData, child: const SettingsPage()); }, + AppLogRoute.name: (routeData) { + return CustomPage( + routeData: routeData, + child: const AppLogPage(), + transitionsBuilder: TransitionsBuilders.slideBottom, + opaque: true, + barrierDismissible: false); + }, HomeRoute.name: (routeData) { return MaterialPageX( routeData: routeData, child: const HomePage()); @@ -218,7 +226,8 @@ class _$AppRouter extends RootStackRouter { RouteConfig(FailedBackupStatusRoute.name, path: '/failed-backup-status-page', guards: [authGuard]), RouteConfig(SettingsRoute.name, - path: '/settings-page', guards: [authGuard]) + path: '/settings-page', guards: [authGuard]), + RouteConfig(AppLogRoute.name, path: '/app-log-page') ]; } @@ -560,6 +569,14 @@ class SettingsRoute extends PageRouteInfo { static const String name = 'SettingsRoute'; } +/// generated route for +/// [AppLogPage] +class AppLogRoute extends PageRouteInfo { + const AppLogRoute() : super(AppLogRoute.name, path: '/app-log-page'); + + static const String name = 'AppLogRoute'; +} + /// generated route for /// [HomePage] class HomeRoute extends PageRouteInfo { diff --git a/mobile/lib/shared/models/immich_logger_message.model.dart b/mobile/lib/shared/models/immich_logger_message.model.dart new file mode 100644 index 0000000000..ae22d97809 --- /dev/null +++ b/mobile/lib/shared/models/immich_logger_message.model.dart @@ -0,0 +1,34 @@ +import 'package:hive/hive.dart'; + +part 'immich_logger_message.model.g.dart'; + +@HiveType(typeId: 3) +class ImmichLoggerMessage { + @HiveField(0) + String message; + + @HiveField(1, defaultValue: "INFO") + String level; + + @HiveField(2) + DateTime createdAt; + + @HiveField(3) + String? context1; + + @HiveField(4) + String? context2; + + ImmichLoggerMessage({ + required this.message, + required this.level, + required this.createdAt, + required this.context1, + required this.context2, + }); + + @override + String toString() { + return 'InAppLoggerMessage(message: $message, level: $level, createdAt: $createdAt)'; + } +} diff --git a/mobile/lib/shared/models/immich_logger_message.model.g.dart b/mobile/lib/shared/models/immich_logger_message.model.g.dart new file mode 100644 index 0000000000000000000000000000000000000000..314d165070d626f190ad3ce4122c36b5816cdf3f GIT binary patch literal 1478 zcmb_cO>f#j5WV|X%qf_Lny>T{Fe$_&MQX5=B2`fpMT*p8H3pW|Zo{V!`(@DgAUsH=iCq-t6a9uh{~ z9AyP-l8|6$b#e;S8PMMlO3?|pylkFlxGqC>v!$aQ^Slz(P9}A-+9s)aLf0fyc7JQP5LdyGzu?lZV96&6Ks1C81I&Tl~3c;+6E8VbX)IiAU&y$5^BZ zM53t6-4m@}s^zA_>YZ3u=Pa%IvD)Lk;352$oZ6Ci^=EdLIV4+kDuGzO7VXxa(#YmG z`cwD#OS1;-R6v^|IY#V?NeAgxK+V^}R}7lntALt)(z#;P9{mcKdln7_;4$y=U$3l)-oGVs#Y6{Ci*>t$&>>u{8hy literal 0 HcmV?d00001 diff --git a/mobile/lib/shared/providers/asset.provider.dart b/mobile/lib/shared/providers/asset.provider.dart index 39df1c58b4..10c2325c97 100644 --- a/mobile/lib/shared/providers/asset.provider.dart +++ b/mobile/lib/shared/providers/asset.provider.dart @@ -1,6 +1,5 @@ import 'dart:collection'; -import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/hive_box.dart'; @@ -10,13 +9,14 @@ import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/services/device_info.service.dart'; import 'package:collection/collection.dart'; import 'package:intl/intl.dart'; +import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; import 'package:photo_manager/photo_manager.dart'; class AssetNotifier extends StateNotifier> { final AssetService _assetService; final AssetCacheService _assetCacheService; - + final log = Logger('AssetNotifier'); final DeviceInfoService _deviceInfoService = DeviceInfoService(); bool _getAllAssetInProgress = false; bool _deleteInProgress = false; @@ -41,7 +41,7 @@ class AssetNotifier extends StateNotifier> { final remoteTask = _assetService.getRemoteAssets(); if (isCacheValid && state.isEmpty) { state = await _assetCacheService.get(); - debugPrint( + log.info( "Reading assets from cache: ${stopwatch.elapsedMilliseconds}ms", ); stopwatch.reset(); @@ -52,25 +52,25 @@ class AssetNotifier extends StateNotifier> { final List currentLocal = state.slice(0, remoteBegin); List? newRemote = await remoteTask; List? newLocal = await localTask; - debugPrint("Load assets: ${stopwatch.elapsedMilliseconds}ms"); + log.info("Load assets: ${stopwatch.elapsedMilliseconds}ms"); stopwatch.reset(); if (newRemote == null && (newLocal == null || currentLocal.equals(newLocal))) { - debugPrint("state is already up-to-date"); + log.info("state is already up-to-date"); return; } newRemote ??= state.slice(remoteBegin); newLocal ??= []; state = _combineLocalAndRemoteAssets(local: newLocal, remote: newRemote); - debugPrint("Combining assets: ${stopwatch.elapsedMilliseconds}ms"); + log.info("Combining assets: ${stopwatch.elapsedMilliseconds}ms"); } finally { _getAllAssetInProgress = false; } - debugPrint("[getAllAsset] setting new asset state"); + log.info("setting new asset state"); stopwatch.reset(); _cacheState(); - debugPrint("Store assets in cache: ${stopwatch.elapsedMilliseconds}ms"); + log.info("Store assets in cache: ${stopwatch.elapsedMilliseconds}ms"); } List _combineLocalAndRemoteAssets({ @@ -155,8 +155,8 @@ class AssetNotifier extends StateNotifier> { if (local.isNotEmpty) { try { return await PhotoManager.editor.deleteWithIds(local); - } catch (e) { - debugPrint("Delete asset from device failed: $e"); + } catch (e, stack) { + log.severe("Failed to delete asset from device", e, stack); } } return []; diff --git a/mobile/lib/shared/providers/release_info.provider.dart b/mobile/lib/shared/providers/release_info.provider.dart index fcdd398cc0..d10a7a07a0 100644 --- a/mobile/lib/shared/providers/release_info.provider.dart +++ b/mobile/lib/shared/providers/release_info.provider.dart @@ -6,10 +6,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:http/http.dart'; import 'package:immich_mobile/constants/hive_box.dart'; import 'package:immich_mobile/shared/views/version_announcement_overlay.dart'; +import 'package:logging/logging.dart'; class ReleaseInfoNotifier extends StateNotifier { ReleaseInfoNotifier() : super(""); - + final log = Logger('ReleaseInfoNotifier'); void checkGithubReleaseInfo() async { final Client client = Client(); var box = Hive.box(hiveGithubReleaseInfoBox); @@ -28,9 +29,6 @@ class ReleaseInfoNotifier extends StateNotifier { String latestTagVersion = data["tag_name"]; state = latestTagVersion; - debugPrint("Local release version $localReleaseVersion"); - debugPrint("Remote release veresion $latestTagVersion"); - if (localReleaseVersion == null && latestTagVersion.isNotEmpty) { VersionAnnouncementOverlayController.appLoader.show(); return; diff --git a/mobile/lib/shared/providers/websocket.provider.dart b/mobile/lib/shared/providers/websocket.provider.dart index bc48762768..9b5e1e87c9 100644 --- a/mobile/lib/shared/providers/websocket.provider.dart +++ b/mobile/lib/shared/providers/websocket.provider.dart @@ -6,23 +6,24 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/hive_box.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart'; +import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; import 'package:socket_io_client/socket_io_client.dart'; -class WebscoketState { +class WebsocketState { final Socket? socket; final bool isConnected; - WebscoketState({ + WebsocketState({ this.socket, required this.isConnected, }); - WebscoketState copyWith({ + WebsocketState copyWith({ Socket? socket, bool? isConnected, }) { - return WebscoketState( + return WebsocketState( socket: socket ?? this.socket, isConnected: isConnected ?? this.isConnected, ); @@ -30,13 +31,13 @@ class WebscoketState { @override String toString() => - 'WebscoketState(socket: $socket, isConnected: $isConnected)'; + 'WebsocketState(socket: $socket, isConnected: $isConnected)'; @override bool operator ==(Object other) { if (identical(this, other)) return true; - return other is WebscoketState && + return other is WebsocketState && other.socket == socket && other.isConnected == isConnected; } @@ -45,12 +46,11 @@ class WebscoketState { int get hashCode => socket.hashCode ^ isConnected.hashCode; } -class WebsocketNotifier extends StateNotifier { +class WebsocketNotifier extends StateNotifier { WebsocketNotifier(this.ref) - : super(WebscoketState(socket: null, isConnected: false)) { - debugPrint("Init websocket instance"); - } + : super(WebsocketState(socket: null, isConnected: false)); + final log = Logger('WebsocketNotifier'); final Ref ref; connect() { @@ -60,8 +60,8 @@ class WebsocketNotifier extends StateNotifier { var accessToken = Hive.box(userInfoBox).get(accessTokenKey); var endpoint = Hive.box(userInfoBox).get(serverEndpointKey); try { - debugPrint("[WEBSOCKET] Attempting to connect to ws"); - // Configure socket transports must be sepecified + log.info("Attempting to connect to websocket"); + // Configure socket transports must be specified Socket socket = io( endpoint.toString().replaceAll('/api', ''), OptionBuilder() @@ -76,18 +76,18 @@ class WebsocketNotifier extends StateNotifier { ); socket.onConnect((_) { - debugPrint("[WEBSOCKET] Established Websocket Connection"); - state = WebscoketState(isConnected: true, socket: socket); + log.info("Established Websocket Connection"); + state = WebsocketState(isConnected: true, socket: socket); }); socket.onDisconnect((_) { - debugPrint("[WEBSOCKET] Disconnect to Websocket Connection"); - state = WebscoketState(isConnected: false, socket: null); + log.info("Disconnect to Websocket Connection"); + state = WebsocketState(isConnected: false, socket: null); }); socket.on('error', (errorMessage) { - debugPrint("Webcoket Error - $errorMessage"); - state = WebscoketState(isConnected: false, socket: null); + log.severe("Websocket Error - $errorMessage"); + state = WebsocketState(isConnected: false, socket: null); }); socket.on('on_upload_success', (data) { @@ -105,21 +105,22 @@ class WebsocketNotifier extends StateNotifier { } disconnect() { - debugPrint("[WEBSOCKET] Attempting to disconnect"); + log.info("Attempting to disconnect from websocket"); + var socket = state.socket?.disconnect(); if (socket?.disconnected == true) { - state = WebscoketState(isConnected: false, socket: null); + state = WebsocketState(isConnected: false, socket: null); } } stopListenToEvent(String eventName) { - debugPrint("[Websocket] Stop listening to event $eventName"); + log.info("Stop listening to event $eventName"); state.socket?.off(eventName); } listenUploadEvent() { - debugPrint("[Websocket] Start listening to event on_upload_success"); + log.info("Start listening to event on_upload_success"); state.socket?.on('on_upload_success', (data) { var jsonString = jsonDecode(data.toString()); AssetResponseDto? newAsset = AssetResponseDto.fromJson(jsonString); @@ -132,6 +133,6 @@ class WebsocketNotifier extends StateNotifier { } final websocketProvider = - StateNotifierProvider((ref) { + StateNotifierProvider((ref) { return WebsocketNotifier(ref); }); diff --git a/mobile/lib/shared/services/immich_logger.service.dart b/mobile/lib/shared/services/immich_logger.service.dart new file mode 100644 index 0000000000..dac4d27a27 --- /dev/null +++ b/mobile/lib/shared/services/immich_logger.service.dart @@ -0,0 +1,87 @@ +import 'dart:io'; + +import 'package:flutter/widgets.dart'; +import 'package:hive/hive.dart'; +import 'package:immich_mobile/constants/hive_box.dart'; +import 'package:immich_mobile/shared/models/immich_logger_message.model.dart'; +import 'package:logging/logging.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:share_plus/share_plus.dart'; + +/// [ImmichLogger] is a custom logger that is built on top of the [logging] package. +/// The logs are written to a Hive box and onto console, using `debugPrint` method. +/// +/// The logs are deleted when exceeding the `maxLogEntries` (default 200) property +/// in the class. +/// +/// Logs can be shared by calling the `shareLogs` method, which will open a share dialog +/// and generate a csv file. +class ImmichLogger { + final maxLogEntries = 200; + final Box _box = Hive.box(immichLoggerBox); + + List get messages => + _box.values.toList().reversed.toList(); + + ImmichLogger() { + _removeOverflowMessages(); + } + + init() { + Logger.root.level = Level.INFO; + Logger.root.onRecord.listen(_writeLogToHiveBox); + } + + _removeOverflowMessages() { + if (_box.length > maxLogEntries) { + var numberOfEntryToBeDeleted = _box.length - maxLogEntries; + for (var i = 0; i < numberOfEntryToBeDeleted; i++) { + _box.deleteAt(0); + } + } + } + + _writeLogToHiveBox(LogRecord record) { + final Box box = Hive.box(immichLoggerBox); + var formattedMessage = record.message; + + debugPrint('[${record.level.name}] [${record.time}] ${record.message}'); + box.add( + ImmichLoggerMessage( + message: formattedMessage, + level: record.level.name, + createdAt: record.time, + context1: record.loggerName, + context2: record.stackTrace + ?.toString(), // Something more useful here? (e.g. stacktrace - I cannot get it to format nicely though) + ), + ); + } + + void clearLogs() { + _box.clear(); + } + + shareLogs() async { + var tempDir = await getTemporaryDirectory(); + var filePath = '${tempDir.path}/${DateTime.now().toIso8601String()}.csv'; + var logFile = await File(filePath).create(); + // Write header + logFile.writeAsStringSync("created_at,context_1,context_2,message,type\n"); + + // Write messages + for (var message in messages) { + logFile.writeAsStringSync( + "${message.createdAt},${message.context1 ?? ""},${message.context2 ?? ""},${message.message},${message.level.toString()}\n", + mode: FileMode.append, + ); + } + + // Share file + Share.shareFiles( + [filePath], + subject: "Immich logs ${DateTime.now().toIso8601String()}", + sharePositionOrigin: Rect.zero, + ); + } +} diff --git a/mobile/lib/shared/views/app_log_page.dart b/mobile/lib/shared/views/app_log_page.dart new file mode 100644 index 0000000000..1306d03eb5 --- /dev/null +++ b/mobile/lib/shared/views/app_log_page.dart @@ -0,0 +1,153 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/shared/services/immich_logger.service.dart'; +import 'package:intl/intl.dart'; + +class AppLogPage extends HookConsumerWidget { + const AppLogPage({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final immichLogger = ImmichLogger(); + final logMessages = useState(immichLogger.messages); + + Widget buildLeadingIcon(String level) { + switch (level) { + case "INFO": + return Container( + width: 10, + height: 10, + decoration: BoxDecoration( + color: Theme.of(context).primaryColor, + borderRadius: BorderRadius.circular(5), + ), + ); + case "SEVERE": + return Container( + width: 10, + height: 10, + decoration: BoxDecoration( + color: Colors.redAccent, + borderRadius: BorderRadius.circular(5), + ), + ); + case "WARNING": + return Container( + width: 10, + height: 10, + decoration: BoxDecoration( + color: Colors.orangeAccent, + borderRadius: BorderRadius.circular(5), + ), + ); + default: + return Container( + width: 10, + height: 10, + decoration: BoxDecoration( + color: Theme.of(context).primaryColor, + borderRadius: BorderRadius.circular(5), + ), + ); + } + } + + getTileColor(String level) { + switch (level) { + case "INFO": + return Colors.transparent; + case "SEVERE": + return Colors.redAccent.withOpacity(0.075); + case "WARNING": + return Colors.orangeAccent.withOpacity(0.075); + default: + return Theme.of(context).primaryColor.withOpacity(0.1); + } + } + + return Scaffold( + appBar: AppBar( + title: Text( + "Logs - ${logMessages.value.length}", + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16.0, + ), + ), + scrolledUnderElevation: 1, + elevation: 2, + actions: [ + IconButton( + icon: Icon( + Icons.delete_outline_rounded, + color: Theme.of(context).primaryColor, + semanticLabel: "Clear logs", + size: 20.0, + ), + onPressed: () { + immichLogger.clearLogs(); + logMessages.value = []; + }, + ), + IconButton( + icon: Icon( + Icons.share_rounded, + color: Theme.of(context).primaryColor, + semanticLabel: "Share logs", + size: 20.0, + ), + onPressed: () { + immichLogger.shareLogs(); + }, + ), + ], + leading: IconButton( + onPressed: () { + AutoRouter.of(context).pop(); + }, + icon: const Icon( + Icons.arrow_back_ios_new_rounded, + size: 20.0, + ), + ), + centerTitle: true, + ), + body: ListView.separated( + separatorBuilder: (context, index) { + return Divider( + height: 0, + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white70 + : Colors.grey[500], + ); + }, + itemCount: logMessages.value.length, + itemBuilder: (context, index) { + var logMessage = logMessages.value[index]; + return ListTile( + visualDensity: VisualDensity.compact, + dense: true, + tileColor: getTileColor(logMessage.level), + minLeadingWidth: 10, + title: Text( + logMessage.message, + style: const TextStyle(fontSize: 14.0, fontFamily: "Inconsolata"), + ), + subtitle: Text( + "[${logMessage.context1}] Logged on ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)}", + style: TextStyle( + fontSize: 12.0, + color: Colors.grey[600], + ), + ), + leading: buildLeadingIcon(logMessage.level), + ); + }, + ), + ); + } +} diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index cbbf11d432..2870a23466 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -266,7 +266,7 @@ packages: name: ffi url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "2.0.1" file: dependency: transitive description: @@ -554,12 +554,12 @@ packages: source: hosted version: "1.0.1" logging: - dependency: transitive + dependency: "direct main" description: name: logging url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.1.0" matcher: dependency: transitive description: @@ -629,7 +629,7 @@ packages: name: package_info_plus url: "https://pub.dartlang.org" source: hosted - version: "1.4.2" + version: "1.4.3+1" package_info_plus_linux: dependency: transitive description: @@ -664,7 +664,7 @@ packages: name: package_info_plus_windows url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "2.1.0" path: dependency: "direct main" description: @@ -699,7 +699,7 @@ packages: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "2.1.6" + version: "2.1.7" path_provider_macos: dependency: transitive description: @@ -720,7 +720,7 @@ packages: name: path_provider_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.6" + version: "2.1.3" pedantic: dependency: transitive description: @@ -998,14 +998,14 @@ packages: name: sqflite url: "https://pub.dartlang.org" source: hosted - version: "2.0.2+1" + version: "2.2.0+3" sqflite_common: dependency: transitive description: name: sqflite_common url: "https://pub.dartlang.org" source: hosted - version: "2.2.1+1" + version: "2.4.0+2" stack_trace: dependency: transitive description: @@ -1257,7 +1257,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.5.2" + version: "2.7.0" wkt_parser: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 0796cc9df8..c479c27c7e 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -47,6 +47,7 @@ dependencies: # easy to remove packages: image_picker: ^0.8.5+3 # only used to select user profile image from system gallery -> we can simply select an image from within immich? + logging: ^1.1.0 dev_dependencies: flutter_test: @@ -71,7 +72,9 @@ flutter: - family: SnowburstOne fonts: - asset: fonts/SnowburstOne.ttf - + - family: Inconsolata + fonts: + - asset: fonts/Inconsolata-Regular.ttf flutter_icons: image_path_android: "assets/immich-logo-no-outline.png" image_path_ios: "assets/immich-logo-no-outline.png" diff --git a/server/apps/immich/src/api-v1/user/user.service.spec.ts b/server/apps/immich/src/api-v1/user/user.service.spec.ts index 9962752cae..8539e88f46 100644 --- a/server/apps/immich/src/api-v1/user/user.service.spec.ts +++ b/server/apps/immich/src/api-v1/user/user.service.spec.ts @@ -1,5 +1,5 @@ import { UserEntity } from '@app/database/entities/user.entity'; -import { BadRequestException, NotFoundException, UnauthorizedException } from '@nestjs/common'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; import { newUserRepositoryMock } from '../../../test/test-utils'; import { AuthUserDto } from '../../decorators/auth-user.decorator'; import { IUserRepository } from './user-repository'; From d82dec9773d36e8707303ef11e69e4ed6114b0f3 Mon Sep 17 00:00:00 2001 From: Fynn Petersen-Frey Date: Mon, 28 Nov 2022 17:01:09 +0100 Subject: [PATCH 24/30] fix(mobile): fix cache invalidation on logout (#1030) await all the cache-invalidation operations during logout and catch errors to actually perform all operations. --- mobile/lib/modules/home/services/asset.service.dart | 4 ++-- .../login/providers/authentication.provider.dart | 13 ++++++++----- mobile/lib/shared/providers/asset.provider.dart | 2 +- mobile/lib/shared/services/json_cache.dart | 10 +++++++--- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/mobile/lib/modules/home/services/asset.service.dart b/mobile/lib/modules/home/services/asset.service.dart index 264f636e71..2d8014484d 100644 --- a/mobile/lib/modules/home/services/asset.service.dart +++ b/mobile/lib/modules/home/services/asset.service.dart @@ -30,11 +30,11 @@ class AssetService { AssetService(this._apiService, this._backupService, this._backgroundService); /// Returns `null` if the server state did not change, else list of assets - Future?> getRemoteAssets() async { + Future?> getRemoteAssets({required bool hasCache}) async { final Box box = Hive.box(userInfoBox); final Pair, String?>? remote = await _apiService .assetApi - .getAllAssetsWithETag(eTag: box.get(assetEtagKey)); + .getAllAssetsWithETag(eTag: hasCache ? box.get(assetEtagKey) : null); if (remote == null) { return null; } diff --git a/mobile/lib/modules/login/providers/authentication.provider.dart b/mobile/lib/modules/login/providers/authentication.provider.dart index 1864d0e024..db1c2343cd 100644 --- a/mobile/lib/modules/login/providers/authentication.provider.dart +++ b/mobile/lib/modules/login/providers/authentication.provider.dart @@ -101,11 +101,14 @@ class AuthenticationNotifier extends StateNotifier { } Future logout() async { - Hive.box(userInfoBox).delete(accessTokenKey); state = state.copyWith(isAuthenticated: false); - _assetCacheService.invalidate(); - _albumCacheService.invalidate(); - _sharedAlbumCacheService.invalidate(); + await Future.wait([ + Hive.box(userInfoBox).delete(accessTokenKey), + Hive.box(userInfoBox).delete(assetEtagKey), + _assetCacheService.invalidate(), + _albumCacheService.invalidate(), + _sharedAlbumCacheService.invalidate(), + ]); // Remove login info from local storage var loginInfo = @@ -115,7 +118,7 @@ class AuthenticationNotifier extends StateNotifier { loginInfo.password = ""; loginInfo.isSaveLogin = false; - Hive.box(hiveLoginInfoBox).put( + await Hive.box(hiveLoginInfoBox).put( savedLoginInfoKey, loginInfo, ); diff --git a/mobile/lib/shared/providers/asset.provider.dart b/mobile/lib/shared/providers/asset.provider.dart index 10c2325c97..9250c33802 100644 --- a/mobile/lib/shared/providers/asset.provider.dart +++ b/mobile/lib/shared/providers/asset.provider.dart @@ -38,7 +38,7 @@ class AssetNotifier extends StateNotifier> { final bool isCacheValid = await _assetCacheService.isValid(); stopwatch.start(); final localTask = _assetService.getLocalAssets(urgent: !isCacheValid); - final remoteTask = _assetService.getRemoteAssets(); + final remoteTask = _assetService.getRemoteAssets(hasCache: isCacheValid); if (isCacheValid && state.isEmpty) { state = await _assetCacheService.get(); log.info( diff --git a/mobile/lib/shared/services/json_cache.dart b/mobile/lib/shared/services/json_cache.dart index b8a403abba..739d8931de 100644 --- a/mobile/lib/shared/services/json_cache.dart +++ b/mobile/lib/shared/services/json_cache.dart @@ -23,8 +23,12 @@ abstract class JsonCache { } Future invalidate() async { - final file = await _getCacheFile(); - await file.delete(); + try { + final file = await _getCacheFile(); + await file.delete(); + } on FileSystemException { + // file is already deleted + } } Future putRawData(dynamic data) async { @@ -46,4 +50,4 @@ abstract class JsonCache { void put(T data); Future get(); -} \ No newline at end of file +} From 765181bbc0df6a1d31abf21fb3b80a00863893e9 Mon Sep 17 00:00:00 2001 From: Fynn Petersen-Frey Date: Mon, 28 Nov 2022 17:17:27 +0100 Subject: [PATCH 25/30] chore(mobile): improve CSV log export (#1032) --- .../services/immich_logger.service.dart | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/mobile/lib/shared/services/immich_logger.service.dart b/mobile/lib/shared/services/immich_logger.service.dart index dac4d27a27..4e7d3bf71c 100644 --- a/mobile/lib/shared/services/immich_logger.service.dart +++ b/mobile/lib/shared/services/immich_logger.service.dart @@ -52,8 +52,7 @@ class ImmichLogger { level: record.level.name, createdAt: record.time, context1: record.loggerName, - context2: record.stackTrace - ?.toString(), // Something more useful here? (e.g. stacktrace - I cannot get it to format nicely though) + context2: record.stackTrace?.toString(), ), ); } @@ -62,26 +61,35 @@ class ImmichLogger { _box.clear(); } - shareLogs() async { - var tempDir = await getTemporaryDirectory(); - var filePath = '${tempDir.path}/${DateTime.now().toIso8601String()}.csv'; - var logFile = await File(filePath).create(); - // Write header - logFile.writeAsStringSync("created_at,context_1,context_2,message,type\n"); + Future shareLogs() async { + final tempDir = await getTemporaryDirectory(); + final dateTime = DateTime.now().toIso8601String(); + final filePath = '${tempDir.path}/Immich_log_$dateTime.csv'; + final logFile = await File(filePath).create(); + final io = logFile.openWrite(); + try { + // Write header + io.write("created_at,level,context,message,stacktrace\n"); - // Write messages - for (var message in messages) { - logFile.writeAsStringSync( - "${message.createdAt},${message.context1 ?? ""},${message.context2 ?? ""},${message.message},${message.level.toString()}\n", - mode: FileMode.append, - ); + // Write messages + for (final m in messages) { + io.write( + '${m.createdAt},${m.level},"${m.context1 ?? ""}","${m.message}","${m.context2 ?? ""}"\n', + ); + } + } finally { + await io.flush(); + await io.close(); } // Share file - Share.shareFiles( + await Share.shareFiles( [filePath], - subject: "Immich logs ${DateTime.now().toIso8601String()}", + subject: "Immich logs $dateTime", sharePositionOrigin: Rect.zero, ); + + // Clean up temp file + await logFile.delete(); } } From cbc979263ebe4f44ca70e74405e991da7a46bcf1 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 28 Nov 2022 14:14:22 -0600 Subject: [PATCH 26/30] chore(mobile): Improve readability of logs page (#1033) --- .../shared/providers/websocket.provider.dart | 12 +-- mobile/lib/shared/views/app_log_page.dart | 86 +++++++++++-------- 2 files changed, 55 insertions(+), 43 deletions(-) diff --git a/mobile/lib/shared/providers/websocket.provider.dart b/mobile/lib/shared/providers/websocket.provider.dart index 9b5e1e87c9..e9f8db40ea 100644 --- a/mobile/lib/shared/providers/websocket.provider.dart +++ b/mobile/lib/shared/providers/websocket.provider.dart @@ -60,7 +60,7 @@ class WebsocketNotifier extends StateNotifier { var accessToken = Hive.box(userInfoBox).get(accessTokenKey); var endpoint = Hive.box(userInfoBox).get(serverEndpointKey); try { - log.info("Attempting to connect to websocket"); + debugPrint("Attempting to connect to websocket"); // Configure socket transports must be specified Socket socket = io( endpoint.toString().replaceAll('/api', ''), @@ -76,12 +76,12 @@ class WebsocketNotifier extends StateNotifier { ); socket.onConnect((_) { - log.info("Established Websocket Connection"); + debugPrint("Established Websocket Connection"); state = WebsocketState(isConnected: true, socket: socket); }); socket.onDisconnect((_) { - log.info("Disconnect to Websocket Connection"); + debugPrint("Disconnect to Websocket Connection"); state = WebsocketState(isConnected: false, socket: null); }); @@ -105,7 +105,7 @@ class WebsocketNotifier extends StateNotifier { } disconnect() { - log.info("Attempting to disconnect from websocket"); + debugPrint("Attempting to disconnect from websocket"); var socket = state.socket?.disconnect(); @@ -115,12 +115,12 @@ class WebsocketNotifier extends StateNotifier { } stopListenToEvent(String eventName) { - log.info("Stop listening to event $eventName"); + debugPrint("Stop listening to event $eventName"); state.socket?.off(eventName); } listenUploadEvent() { - log.info("Start listening to event on_upload_success"); + debugPrint("Start listening to event on_upload_success"); state.socket?.on('on_upload_success', (data) { var jsonString = jsonDecode(data.toString()); AssetResponseDto? newAsset = AssetResponseDto.fromJson(jsonString); diff --git a/mobile/lib/shared/views/app_log_page.dart b/mobile/lib/shared/views/app_log_page.dart index 1306d03eb5..ff3b2e71c2 100644 --- a/mobile/lib/shared/views/app_log_page.dart +++ b/mobile/lib/shared/views/app_log_page.dart @@ -15,44 +15,33 @@ class AppLogPage extends HookConsumerWidget { final immichLogger = ImmichLogger(); final logMessages = useState(immichLogger.messages); + Widget colorStatusIndicator(Color color) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 10, + height: 10, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + ), + ), + ], + ); + } + Widget buildLeadingIcon(String level) { switch (level) { case "INFO": - return Container( - width: 10, - height: 10, - decoration: BoxDecoration( - color: Theme.of(context).primaryColor, - borderRadius: BorderRadius.circular(5), - ), - ); + return colorStatusIndicator(Theme.of(context).primaryColor); case "SEVERE": - return Container( - width: 10, - height: 10, - decoration: BoxDecoration( - color: Colors.redAccent, - borderRadius: BorderRadius.circular(5), - ), - ); + return colorStatusIndicator(Colors.redAccent); + case "WARNING": - return Container( - width: 10, - height: 10, - decoration: BoxDecoration( - color: Colors.orangeAccent, - borderRadius: BorderRadius.circular(5), - ), - ); + return colorStatusIndicator(Colors.orangeAccent); default: - return Container( - width: 10, - height: 10, - decoration: BoxDecoration( - color: Theme.of(context).primaryColor, - borderRadius: BorderRadius.circular(5), - ), - ); + return colorStatusIndicator(Colors.grey); } } @@ -61,9 +50,13 @@ class AppLogPage extends HookConsumerWidget { case "INFO": return Colors.transparent; case "SEVERE": - return Colors.redAccent.withOpacity(0.075); + return Theme.of(context).brightness == Brightness.dark + ? Colors.redAccent.withOpacity(0.25) + : Colors.redAccent.withOpacity(0.075); case "WARNING": - return Colors.orangeAccent.withOpacity(0.075); + return Theme.of(context).brightness == Brightness.dark + ? Colors.orangeAccent.withOpacity(0.25) + : Colors.orangeAccent.withOpacity(0.075); default: return Theme.of(context).primaryColor.withOpacity(0.1); } @@ -122,7 +115,7 @@ class AppLogPage extends HookConsumerWidget { height: 0, color: Theme.of(context).brightness == Brightness.dark ? Colors.white70 - : Colors.grey[500], + : Colors.grey[600], ); }, itemCount: logMessages.value.length, @@ -133,8 +126,27 @@ class AppLogPage extends HookConsumerWidget { dense: true, tileColor: getTileColor(logMessage.level), minLeadingWidth: 10, - title: Text( - logMessage.message, + title: Text.rich( + TextSpan( + children: [ + TextSpan( + text: "#$index ", + style: TextStyle( + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white70 + : Colors.grey[600], + fontSize: 14.0, + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: logMessage.message, + style: const TextStyle( + fontSize: 14.0, + ), + ), + ], + ), style: const TextStyle(fontSize: 14.0, fontFamily: "Inconsolata"), ), subtitle: Text( From 1068c4ad23e61e7c491d1b0a35d1de615ba710a1 Mon Sep 17 00:00:00 2001 From: Fynn Petersen-Frey Date: Tue, 29 Nov 2022 22:45:47 +0100 Subject: [PATCH 27/30] feat(server,web): activate ETags for all API endpoints and asset serving (#1031) This greatly reduces the network traffic by app/web. --- .../src/api-v1/asset/asset.controller.ts | 30 +++----- .../immich/src/api-v1/asset/asset.service.ts | 72 ++++++++++++------- server/apps/immich/src/main.ts | 1 + server/apps/immich/src/types/index.d.ts | 5 -- server/apps/immich/src/utils/etag.ts | 10 --- server/immich-openapi-specs.json | 2 +- 6 files changed, 57 insertions(+), 63 deletions(-) delete mode 100644 server/apps/immich/src/types/index.d.ts delete mode 100644 server/apps/immich/src/utils/etag.ts diff --git a/server/apps/immich/src/api-v1/asset/asset.controller.ts b/server/apps/immich/src/api-v1/asset/asset.controller.ts index 8042eba02c..94624e0e48 100644 --- a/server/apps/immich/src/api-v1/asset/asset.controller.ts +++ b/server/apps/immich/src/api-v1/asset/asset.controller.ts @@ -14,7 +14,6 @@ import { Header, Put, UploadedFiles, - Request, } from '@nestjs/common'; import { Authenticated } from '../../decorators/authenticated.decorator'; import { AssetService } from './asset.service'; @@ -22,12 +21,12 @@ import { FileFieldsInterceptor } from '@nestjs/platform-express'; import { assetUploadOption } from '../../config/asset-upload.config'; import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator'; import { ServeFileDto } from './dto/serve-file.dto'; -import { Response as Res, Request as Req } from 'express'; +import { Response as Res} from 'express'; import { BackgroundTaskService } from '../../modules/background-task/background-task.service'; import { DeleteAssetDto } from './dto/delete-asset.dto'; import { SearchAssetDto } from './dto/search-asset.dto'; import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto'; -import { ApiBearerAuth, ApiBody, ApiConsumes, ApiHeader, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger'; import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto'; import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto'; import { AssetResponseDto } from './response-dto/asset-response.dto'; @@ -50,7 +49,6 @@ import { IMMICH_ARCHIVE_FILE_COUNT, IMMICH_CONTENT_LENGTH_HINT, } from '../../constants/download.constant'; -import { etag } from '../../utils/etag'; @Authenticated() @ApiBearerAuth() @@ -110,7 +108,7 @@ export class AssetController { } @Get('/file/:assetId') - @Header('Cache-Control', 'max-age=300') + @Header('Cache-Control', 'max-age=3600') async serveFile( @Headers() headers: Record, @Response({ passthrough: true }) res: Res, @@ -121,13 +119,14 @@ export class AssetController { } @Get('/thumbnail/:assetId') - @Header('Cache-Control', 'max-age=300') + @Header('Cache-Control', 'max-age=3600') async getAssetThumbnail( + @Headers() headers: Record, @Response({ passthrough: true }) res: Res, @Param('assetId') assetId: string, @Query(new ValidationPipe({ transform: true })) query: GetAssetThumbnailDto, ): Promise { - return this.assetService.getAssetThumbnail(assetId, query, res); + return this.assetService.getAssetThumbnail(assetId, query, res, headers); } @Get('/curated-objects') @@ -176,22 +175,9 @@ export class AssetController { required: false, schema: { type: 'string' }, }) - @ApiResponse({ - status: 200, - headers: { ETag: { required: true, schema: { type: 'string' } } }, - type: [AssetResponseDto], - }) - async getAllAssets(@GetAuthUser() authUser: AuthUserDto, @Response() response: Res, @Request() request: Req) { + async getAllAssets(@GetAuthUser() authUser: AuthUserDto): Promise { const assets = await this.assetService.getAllAssets(authUser); - const clientEtag = request.headers['if-none-match']; - const json = JSON.stringify(assets); - const serverEtag = await etag(json); - response.setHeader('ETag', serverEtag); - if (clientEtag === serverEtag) { - response.status(304).end(); - } else { - response.contentType('application/json').status(200).send(json); - } + return assets; } @Post('/time-bucket') diff --git a/server/apps/immich/src/api-v1/asset/asset.service.ts b/server/apps/immich/src/api-v1/asset/asset.service.ts index 74217abf5b..df71d58837 100644 --- a/server/apps/immich/src/api-v1/asset/asset.service.ts +++ b/server/apps/immich/src/api-v1/asset/asset.service.ts @@ -306,7 +306,12 @@ export class AssetService { } } - public async getAssetThumbnail(assetId: string, query: GetAssetThumbnailDto, res: Res) { + public async getAssetThumbnail( + assetId: string, + query: GetAssetThumbnailDto, + res: Res, + headers: Record, + ) { let fileReadStream: ReadStream; const asset = await this.assetRepository.findOne({ where: { id: assetId } }); @@ -316,28 +321,22 @@ export class AssetService { } try { - if (query.format == GetAssetThumbnailFormatEnum.JPEG) { + if (query.format == GetAssetThumbnailFormatEnum.WEBP && asset.webpPath && asset.webpPath.length > 0) { + if (await processETag(asset.webpPath, res, headers)) { + return; + } + await fs.access(asset.webpPath, constants.R_OK); + fileReadStream = createReadStream(asset.webpPath); + } else { if (!asset.resizePath) { throw new NotFoundException('resizePath not set'); } - - await fs.access(asset.resizePath, constants.R_OK | constants.W_OK); - fileReadStream = createReadStream(asset.resizePath); - } else { - if (asset.webpPath && asset.webpPath.length > 0) { - await fs.access(asset.webpPath, constants.R_OK | constants.W_OK); - fileReadStream = createReadStream(asset.webpPath); - } else { - if (!asset.resizePath) { - throw new NotFoundException('resizePath not set'); - } - - await fs.access(asset.resizePath, constants.R_OK | constants.W_OK); - fileReadStream = createReadStream(asset.resizePath); + if (await processETag(asset.resizePath, res, headers)) { + return; } + await fs.access(asset.resizePath, constants.R_OK); + fileReadStream = createReadStream(asset.resizePath); } - - res.header('Cache-Control', 'max-age=300'); return new StreamableFile(fileReadStream); } catch (e) { res.header('Cache-Control', 'none'); @@ -349,7 +348,7 @@ export class AssetService { } } - public async serveFile(assetId: string, query: ServeFileDto, res: Res, headers: any) { + public async serveFile(assetId: string, query: ServeFileDto, res: Res, headers: Record) { let fileReadStream: ReadStream; const asset = await this._assetRepository.getById(assetId); @@ -371,6 +370,9 @@ export class AssetService { Logger.error('Error serving IMAGE asset for web', 'ServeFile'); throw new InternalServerErrorException(`Failed to serve image asset for web`, 'ServeFile'); } + if (await processETag(asset.resizePath, res, headers)) { + return; + } await fs.access(asset.resizePath, constants.R_OK | constants.W_OK); fileReadStream = createReadStream(asset.resizePath); @@ -384,7 +386,9 @@ export class AssetService { res.set({ 'Content-Type': asset.mimeType, }); - + if (await processETag(asset.originalPath, res, headers)) { + return; + } await fs.access(asset.originalPath, constants.R_OK | constants.W_OK); fileReadStream = createReadStream(asset.originalPath); } else { @@ -392,7 +396,9 @@ export class AssetService { res.set({ 'Content-Type': 'image/webp', }); - + if (await processETag(asset.webpPath, res, headers)) { + return; + } await fs.access(asset.webpPath, constants.R_OK | constants.W_OK); fileReadStream = createReadStream(asset.webpPath); } else { @@ -403,6 +409,9 @@ export class AssetService { if (!asset.resizePath) { throw new Error('resizePath not set'); } + if (await processETag(asset.resizePath, res, headers)) { + return; + } await fs.access(asset.resizePath, constants.R_OK | constants.W_OK); fileReadStream = createReadStream(asset.resizePath); @@ -436,9 +445,9 @@ export class AssetService { if (range) { /** Extracting Start and End value from Range Header */ - let [start, end] = range.replace(/bytes=/, '').split('-'); - start = parseInt(start, 10); - end = end ? parseInt(end, 10) : size - 1; + const [startStr, endStr] = range.replace(/bytes=/, '').split('-'); + let start = parseInt(startStr, 10); + let end = endStr ? parseInt(endStr, 10) : size - 1; if (!isNaN(start) && isNaN(end)) { start = start; @@ -475,7 +484,9 @@ export class AssetService { res.set({ 'Content-Type': mimeType, }); - + if (await processETag(asset.originalPath, res, headers)) { + return; + } return new StreamableFile(createReadStream(videoPath)); } } catch (e) { @@ -632,3 +643,14 @@ export class AssetService { return this._assetRepository.getAssetCountByUserId(authUser.id); } } + +async function processETag(path: string, res: Res, headers: Record): Promise { + const { size, mtimeNs } = await fs.stat(path, { bigint: true }); + const etag = `W/"${size}-${mtimeNs}"`; + res.setHeader('ETag', etag); + if (etag === headers['if-none-match']) { + res.status(304); + return true; + } + return false; +} diff --git a/server/apps/immich/src/main.ts b/server/apps/immich/src/main.ts index c744c0422a..cd1cd22d23 100644 --- a/server/apps/immich/src/main.ts +++ b/server/apps/immich/src/main.ts @@ -14,6 +14,7 @@ async function bootstrap() { const app = await NestFactory.create(AppModule); app.set('trust proxy'); + app.set('etag', 'strong'); app.use(cookieParser()); app.use(json({ limit: '10mb' })); if (process.env.NODE_ENV === 'development') { diff --git a/server/apps/immich/src/types/index.d.ts b/server/apps/immich/src/types/index.d.ts deleted file mode 100644 index ad4d7460b8..0000000000 --- a/server/apps/immich/src/types/index.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -declare module 'crypto' { - namespace webcrypto { - const subtle: SubtleCrypto; - } -} \ No newline at end of file diff --git a/server/apps/immich/src/utils/etag.ts b/server/apps/immich/src/utils/etag.ts deleted file mode 100644 index 294695613f..0000000000 --- a/server/apps/immich/src/utils/etag.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { webcrypto } from 'node:crypto'; -const { subtle } = webcrypto; - -export async function etag(text: string): Promise { - const encoder = new TextEncoder(); - const data = encoder.encode(text); - const buffer = await subtle.digest('SHA-1', data); - const hash = Buffer.from(buffer).toString('base64').slice(0, 27); - return `"${data.length}-${hash}"`; -} \ No newline at end of file diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index d4961228c6..bee972c82f 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -1 +1 @@ -{"openapi":"3.0.0","paths":{"/user":{"get":{"operationId":"getAllUsers","parameters":[{"name":"isAll","required":true,"in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}}}}}},"tags":["User"],"security":[{"bearer":[]}]},"post":{"operationId":"createUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]},"put":{"operationId":"updateUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/info/{userId}":{"get":{"operationId":"getUserById","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"]}},"/user/me":{"get":{"operationId":"getMyUserInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/count":{"get":{"operationId":"getUserCount","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserCountResponseDto"}}}}},"tags":["User"]}},"/user/{userId}":{"delete":{"operationId":"deleteUser","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/{userId}/restore":{"post":{"operationId":"restoreUser","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/profile-image":{"post":{"operationId":"createProfileImage","parameters":[],"requestBody":{"required":true,"description":"A new avatar for the user","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/CreateProfileImageDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProfileImageResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/profile-image/{userId}":{"get":{"operationId":"getProfileImage","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["User"]}},"/asset/upload":{"post":{"operationId":"uploadFile","parameters":[],"requestBody":{"required":true,"description":"Asset Upload Information","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/AssetFileUploadDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetFileUploadResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/download/{assetId}":{"get":{"operationId":"downloadFile","parameters":[{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}},{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/download-library":{"get":{"operationId":"downloadLibrary","parameters":[{"name":"skip","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/file/{assetId}":{"get":{"operationId":"serveFile","parameters":[{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}},{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/thumbnail/{assetId}":{"get":{"operationId":"getAssetThumbnail","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}},{"name":"format","required":false,"in":"query","schema":{"$ref":"#/components/schemas/ThumbnailFormat"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/curated-objects":{"get":{"operationId":"getCuratedObjects","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedObjectsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/curated-locations":{"get":{"operationId":"getCuratedLocations","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedLocationsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search-terms":{"get":{"operationId":"getAssetSearchTerms","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search":{"post":{"operationId":"searchAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchAssetDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/count-by-time-bucket":{"post":{"operationId":"getAssetCountByTimeBucket","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAssetCountByTimeBucketDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCountByTimeBucketResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/count-by-user-id":{"get":{"operationId":"getAssetCountByUserId","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCountByUserIdResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset":{"get":{"operationId":"getAllAssets","summary":"","description":"Get all AssetEntity belong to the user","parameters":[{"name":"if-none-match","in":"header","description":"ETag of data already cached on the client","required":false,"schema":{"type":"string"}}],"responses":{"200":{"headers":{"ETag":{"required":true,"schema":{"type":"string"}}},"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DeleteAssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/time-bucket":{"post":{"operationId":"getAssetByTimeBucket","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAssetByTimeBucketDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/{deviceId}":{"get":{"operationId":"getUserAssetsByDeviceId","summary":"","description":"Get all asset of a device that are in the database, ID only.","parameters":[{"name":"deviceId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/assetById/{assetId}":{"get":{"operationId":"getAssetById","summary":"","description":"Get a single asset's information","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]},"put":{"operationId":"updateAssetById","summary":"","description":"Update an asset","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/check":{"post":{"operationId":"checkDuplicateAsset","summary":"","description":"Check duplicated asset before uploading - for Web upload used","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/exist":{"post":{"operationId":"checkExistingAssets","summary":"","description":"Checks if multiple assets exist on the server and returns all existing - used by background backup","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckExistingAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckExistingAssetsResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/auth/login":{"post":{"operationId":"login","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginCredentialDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponseDto"}}}}},"tags":["Authentication"]}},"/auth/admin-sign-up":{"post":{"operationId":"adminSignUp","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignUpDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSignupResponseDto"}}}},"400":{"description":"The server already has an admin"}},"tags":["Authentication"]}},"/auth/validateToken":{"post":{"operationId":"validateAccessToken","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateAccessTokenResponseDto"}}}}},"tags":["Authentication"],"security":[{"bearer":[]}]}},"/auth/logout":{"post":{"operationId":"logout","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogoutResponseDto"}}}}},"tags":["Authentication"]}},"/oauth/config":{"post":{"operationId":"generateConfig","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthConfigDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthConfigResponseDto"}}}}},"tags":["OAuth"]}},"/oauth/callback":{"post":{"operationId":"callback","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthCallbackDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponseDto"}}}}},"tags":["OAuth"]}},"/device-info":{"post":{"operationId":"createDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDeviceInfoDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDeviceInfoDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]}},"/server-info":{"get":{"operationId":"getServerInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerInfoResponseDto"}}}}},"tags":["Server Info"]}},"/server-info/ping":{"get":{"operationId":"pingServer","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerPingResponse"}}}}},"tags":["Server Info"]}},"/server-info/version":{"get":{"operationId":"getServerVersion","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerVersionReponseDto"}}}}},"tags":["Server Info"]}},"/server-info/stats":{"get":{"operationId":"getStats","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerStatsResponseDto"}}}}},"tags":["Server Info"]}},"/album/count-by-user-id":{"get":{"operationId":"getAlbumCountByUserId","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumCountResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album":{"post":{"operationId":"createAlbum","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAlbumDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"get":{"operationId":"getAllAlbums","parameters":[{"name":"shared","required":false,"in":"query","schema":{"type":"boolean"}},{"name":"assetId","required":false,"in":"query","description":"Only returns albums that contain the asset\nIgnores the shared parameter\nundefined: get all albums","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/users":{"put":{"operationId":"addUsersToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddUsersDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/assets":{"put":{"operationId":"addAssetsToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddAssetsResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"removeAssetFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}":{"get":{"operationId":"getAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAlbumDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/user/{userId}":{"delete":{"operationId":"removeUserFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}},{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/download":{"get":{"operationId":"downloadArchive","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}},{"name":"skip","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/jobs":{"get":{"operationId":"getAllJobsStatus","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AllJobStatusResponseDto"}}}}},"tags":["Job"],"security":[{"bearer":[]}]}},"/jobs/{jobId}":{"get":{"operationId":"getJobStatus","parameters":[{"name":"jobId","required":true,"in":"path","schema":{"$ref":"#/components/schemas/JobId"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobStatusResponseDto"}}}}},"tags":["Job"],"security":[{"bearer":[]}]},"put":{"operationId":"sendJobCommand","parameters":[{"name":"jobId","required":true,"in":"path","schema":{"$ref":"#/components/schemas/JobId"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobCommandDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"number"}}}}},"tags":["Job"],"security":[{"bearer":[]}]}},"/system-config":{"get":{"operationId":"getConfig","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemConfigResponseDto"}}}}},"tags":["System Config"],"security":[{"bearer":[]}]},"put":{"operationId":"updateConfig","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSystemConfigDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemConfigResponseDto"}}}}},"tags":["System Config"],"security":[{"bearer":[]}]}}},"info":{"title":"Immich","description":"Immich API","version":"1.17.0","contact":{}},"tags":[],"servers":[{"url":"/api"}],"components":{"securitySchemes":{"bearer":{"scheme":"Bearer","bearerFormat":"JWT","type":"http","name":"JWT","description":"Enter JWT token","in":"header"}},"schemas":{"UserResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"},"profileImagePath":{"type":"string"},"shouldChangePassword":{"type":"boolean"},"isAdmin":{"type":"boolean"},"deletedAt":{"format":"date-time","type":"string"}},"required":["id","email","firstName","lastName","createdAt","profileImagePath","shouldChangePassword","isAdmin"]},"CreateUserDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"John"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"UserCountResponseDto":{"type":"object","properties":{"userCount":{"type":"integer"}},"required":["userCount"]},"UpdateUserDto":{"type":"object","properties":{"id":{"type":"string"},"password":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"isAdmin":{"type":"boolean"},"shouldChangePassword":{"type":"boolean"},"profileImagePath":{"type":"string"}},"required":["id"]},"CreateProfileImageDto":{"type":"object","properties":{"file":{"type":"string","format":"binary"}},"required":["file"]},"CreateProfileImageResponseDto":{"type":"object","properties":{"userId":{"type":"string"},"profileImagePath":{"type":"string"}},"required":["userId","profileImagePath"]},"AssetFileUploadDto":{"type":"object","properties":{"assetData":{"type":"string","format":"binary"}},"required":["assetData"]},"AssetFileUploadResponseDto":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]},"ThumbnailFormat":{"type":"string","enum":["JPEG","WEBP"]},"CuratedObjectsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","object","resizePath","deviceAssetId","deviceId"]},"CuratedLocationsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"city":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","city","resizePath","deviceAssetId","deviceId"]},"SearchAssetDto":{"type":"object","properties":{"searchTerm":{"type":"string"}},"required":["searchTerm"]},"AssetTypeEnum":{"type":"string","enum":["IMAGE","VIDEO","AUDIO","OTHER"]},"ExifResponseDto":{"type":"object","properties":{"id":{"type":"integer","nullable":true,"default":null,"format":"int64"},"fileSizeInByte":{"type":"integer","nullable":true,"default":null,"format":"int64"},"make":{"type":"string","nullable":true,"default":null},"model":{"type":"string","nullable":true,"default":null},"imageName":{"type":"string","nullable":true,"default":null},"exifImageWidth":{"type":"number","nullable":true,"default":null},"exifImageHeight":{"type":"number","nullable":true,"default":null},"orientation":{"type":"string","nullable":true,"default":null},"dateTimeOriginal":{"format":"date-time","type":"string","nullable":true,"default":null},"modifyDate":{"format":"date-time","type":"string","nullable":true,"default":null},"lensModel":{"type":"string","nullable":true,"default":null},"fNumber":{"type":"number","nullable":true,"default":null},"focalLength":{"type":"number","nullable":true,"default":null},"iso":{"type":"number","nullable":true,"default":null},"exposureTime":{"type":"number","nullable":true,"default":null},"latitude":{"type":"number","nullable":true,"default":null},"longitude":{"type":"number","nullable":true,"default":null},"city":{"type":"string","nullable":true,"default":null},"state":{"type":"string","nullable":true,"default":null},"country":{"type":"string","nullable":true,"default":null}}},"SmartInfoResponseDto":{"type":"object","properties":{"id":{"type":"string"},"tags":{"nullable":true,"type":"array","items":{"type":"string"}},"objects":{"nullable":true,"type":"array","items":{"type":"string"}}}},"AssetResponseDto":{"type":"object","properties":{"type":{"$ref":"#/components/schemas/AssetTypeEnum"},"id":{"type":"string"},"deviceAssetId":{"type":"string"},"ownerId":{"type":"string"},"deviceId":{"type":"string"},"originalPath":{"type":"string"},"resizePath":{"type":"string","nullable":true},"createdAt":{"type":"string"},"modifiedAt":{"type":"string"},"isFavorite":{"type":"boolean"},"mimeType":{"type":"string","nullable":true},"duration":{"type":"string"},"webpPath":{"type":"string","nullable":true},"encodedVideoPath":{"type":"string","nullable":true},"exifInfo":{"$ref":"#/components/schemas/ExifResponseDto"},"smartInfo":{"$ref":"#/components/schemas/SmartInfoResponseDto"},"livePhotoVideoId":{"type":"string","nullable":true}},"required":["type","id","deviceAssetId","ownerId","deviceId","originalPath","resizePath","createdAt","modifiedAt","isFavorite","mimeType","duration","webpPath"]},"TimeGroupEnum":{"type":"string","enum":["day","month"]},"GetAssetCountByTimeBucketDto":{"type":"object","properties":{"timeGroup":{"$ref":"#/components/schemas/TimeGroupEnum"}},"required":["timeGroup"]},"AssetCountByTimeBucket":{"type":"object","properties":{"timeBucket":{"type":"string"},"count":{"type":"integer"}},"required":["timeBucket","count"]},"AssetCountByTimeBucketResponseDto":{"type":"object","properties":{"totalCount":{"type":"integer"},"buckets":{"type":"array","items":{"$ref":"#/components/schemas/AssetCountByTimeBucket"}}},"required":["totalCount","buckets"]},"AssetCountByUserIdResponseDto":{"type":"object","properties":{"audio":{"type":"integer","default":0},"photos":{"type":"integer","default":0},"videos":{"type":"integer","default":0},"other":{"type":"integer","default":0},"total":{"type":"integer","default":0}},"required":["audio","photos","videos","other","total"]},"GetAssetByTimeBucketDto":{"type":"object","properties":{"timeBucket":{"title":"Array of date time buckets","example":["2015-06-01T00:00:00.000Z","2016-02-01T00:00:00.000Z","2016-03-01T00:00:00.000Z"],"type":"array","items":{"type":"string"}}},"required":["timeBucket"]},"UpdateAssetDto":{"type":"object","properties":{"isFavorite":{"type":"boolean"}},"required":["isFavorite"]},"DeleteAssetDto":{"type":"object","properties":{"ids":{"title":"Array of asset IDs to delete","example":["bf973405-3f2a-48d2-a687-2ed4167164be","dd41870b-5d00-46d2-924e-1d8489a0aa0f","fad77c3f-deef-4e7e-9608-14c1aa4e559a"],"type":"array","items":{"type":"string"}}},"required":["ids"]},"DeleteAssetStatus":{"type":"string","enum":["SUCCESS","FAILED"]},"DeleteAssetResponseDto":{"type":"object","properties":{"status":{"$ref":"#/components/schemas/DeleteAssetStatus"},"id":{"type":"string"}},"required":["status","id"]},"CheckDuplicateAssetDto":{"type":"object","properties":{"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["deviceAssetId","deviceId"]},"CheckDuplicateAssetResponseDto":{"type":"object","properties":{"isExist":{"type":"boolean"},"id":{"type":"string"}},"required":["isExist"]},"CheckExistingAssetsDto":{"type":"object","properties":{"deviceAssetIds":{"type":"array","items":{"type":"string"}},"deviceId":{"type":"string"}},"required":["deviceAssetIds","deviceId"]},"CheckExistingAssetsResponseDto":{"type":"object","properties":{"existingIds":{"type":"array","items":{"type":"string"}}},"required":["existingIds"]},"LoginCredentialDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"}},"required":["email","password"]},"LoginResponseDto":{"type":"object","properties":{"accessToken":{"type":"string","readOnly":true},"userId":{"type":"string","readOnly":true},"userEmail":{"type":"string","readOnly":true},"firstName":{"type":"string","readOnly":true},"lastName":{"type":"string","readOnly":true},"profileImagePath":{"type":"string","readOnly":true},"isAdmin":{"type":"boolean","readOnly":true},"shouldChangePassword":{"type":"boolean","readOnly":true}},"required":["accessToken","userId","userEmail","firstName","lastName","profileImagePath","isAdmin","shouldChangePassword"]},"SignUpDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"Admin"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"AdminSignupResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"}},"required":["id","email","firstName","lastName","createdAt"]},"ValidateAccessTokenResponseDto":{"type":"object","properties":{"authStatus":{"type":"boolean"}},"required":["authStatus"]},"LogoutResponseDto":{"type":"object","properties":{"successful":{"type":"boolean","readOnly":true},"redirectUri":{"type":"string","readOnly":true}},"required":["successful","redirectUri"]},"OAuthConfigDto":{"type":"object","properties":{"redirectUri":{"type":"string"}},"required":["redirectUri"]},"OAuthConfigResponseDto":{"type":"object","properties":{"enabled":{"type":"boolean","readOnly":true},"url":{"type":"string","readOnly":true},"buttonText":{"type":"string","readOnly":true}},"required":["enabled"]},"OAuthCallbackDto":{"type":"object","properties":{"url":{"type":"string"}},"required":["url"]},"DeviceTypeEnum":{"type":"string","enum":["IOS","ANDROID","WEB"]},"CreateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"DeviceInfoResponseDto":{"type":"object","properties":{"id":{"type":"integer"},"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"userId":{"type":"string"},"deviceId":{"type":"string"},"createdAt":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["id","deviceType","userId","deviceId","createdAt","isAutoBackup"]},"UpdateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"ServerInfoResponseDto":{"type":"object","properties":{"diskSizeRaw":{"type":"integer","format":"int64"},"diskUseRaw":{"type":"integer","format":"int64"},"diskAvailableRaw":{"type":"integer","format":"int64"},"diskUsagePercentage":{"type":"number","format":"float"},"diskSize":{"type":"string"},"diskUse":{"type":"string"},"diskAvailable":{"type":"string"}},"required":["diskSizeRaw","diskUseRaw","diskAvailableRaw","diskUsagePercentage","diskSize","diskUse","diskAvailable"]},"ServerPingResponse":{"type":"object","properties":{"res":{"type":"string","readOnly":true,"example":"pong"}},"required":["res"]},"ServerVersionReponseDto":{"type":"object","properties":{"major":{"type":"integer"},"minor":{"type":"integer"},"patch":{"type":"integer"},"build":{"type":"integer"}},"required":["major","minor","patch","build"]},"UsageByUserDto":{"type":"object","properties":{"userId":{"type":"string"},"videos":{"type":"integer"},"photos":{"type":"integer"},"usageRaw":{"type":"integer","format":"int64"},"usage":{"type":"string"}},"required":["userId","videos","photos","usageRaw","usage"]},"ServerStatsResponseDto":{"type":"object","properties":{"photos":{"type":"integer"},"videos":{"type":"integer"},"objects":{"type":"integer"},"usageRaw":{"type":"integer","format":"int64"},"usage":{"type":"string"},"usageByUser":{"title":"Array of usage for each user","example":[{"photos":1,"videos":1,"diskUsageRaw":1}],"type":"array","items":{"$ref":"#/components/schemas/UsageByUserDto"}}},"required":["photos","videos","objects","usageRaw","usage","usageByUser"]},"AlbumCountResponseDto":{"type":"object","properties":{"owned":{"type":"integer"},"shared":{"type":"integer"},"sharing":{"type":"integer"}},"required":["owned","shared","sharing"]},"CreateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"sharedWithUserIds":{"type":"array","items":{"type":"string"}},"assetIds":{"type":"array","items":{"type":"string"}}},"required":["albumName"]},"AlbumResponseDto":{"type":"object","properties":{"assetCount":{"type":"integer"},"id":{"type":"string"},"ownerId":{"type":"string"},"albumName":{"type":"string"},"createdAt":{"type":"string"},"albumThumbnailAssetId":{"type":"string","nullable":true},"shared":{"type":"boolean"},"sharedUsers":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}},"assets":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}},"required":["assetCount","id","ownerId","albumName","createdAt","albumThumbnailAssetId","shared","sharedUsers","assets"]},"AddUsersDto":{"type":"object","properties":{"sharedUserIds":{"type":"array","items":{"type":"string"}}},"required":["sharedUserIds"]},"AddAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"AddAssetsResponseDto":{"type":"object","properties":{"successfullyAdded":{"type":"integer"},"alreadyInAlbum":{"type":"array","items":{"type":"string"}},"album":{"$ref":"#/components/schemas/AlbumResponseDto"}},"required":["successfullyAdded","alreadyInAlbum"]},"RemoveAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"UpdateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"albumThumbnailAssetId":{"type":"string"}}},"JobCounts":{"type":"object","properties":{"active":{"type":"integer"},"completed":{"type":"integer"},"failed":{"type":"integer"},"delayed":{"type":"integer"},"waiting":{"type":"integer"}},"required":["active","completed","failed","delayed","waiting"]},"AllJobStatusResponseDto":{"type":"object","properties":{"thumbnailGenerationQueueCount":{"$ref":"#/components/schemas/JobCounts"},"metadataExtractionQueueCount":{"$ref":"#/components/schemas/JobCounts"},"videoConversionQueueCount":{"$ref":"#/components/schemas/JobCounts"},"machineLearningQueueCount":{"$ref":"#/components/schemas/JobCounts"},"isThumbnailGenerationActive":{"type":"boolean"},"isMetadataExtractionActive":{"type":"boolean"},"isVideoConversionActive":{"type":"boolean"},"isMachineLearningActive":{"type":"boolean"}},"required":["thumbnailGenerationQueueCount","metadataExtractionQueueCount","videoConversionQueueCount","machineLearningQueueCount","isThumbnailGenerationActive","isMetadataExtractionActive","isVideoConversionActive","isMachineLearningActive"]},"JobId":{"type":"string","enum":["thumbnail-generation","metadata-extraction","video-conversion","machine-learning"]},"JobStatusResponseDto":{"type":"object","properties":{"isActive":{"type":"boolean"},"queueCount":{"type":"object"}},"required":["isActive","queueCount"]},"JobCommand":{"type":"string","enum":["start","stop"]},"JobCommandDto":{"type":"object","properties":{"command":{"$ref":"#/components/schemas/JobCommand"}},"required":["command"]},"SystemConfigKey":{"type":"string","enum":["ffmpeg_crf","ffmpeg_preset","ffmpeg_target_video_codec","ffmpeg_target_audio_codec","ffmpeg_target_scaling"]},"SystemConfigResponseItem":{"type":"object","properties":{"name":{"type":"string"},"key":{"$ref":"#/components/schemas/SystemConfigKey"},"value":{"type":"string"},"defaultValue":{"type":"string"}},"required":["name","key","value","defaultValue"]},"SystemConfigResponseDto":{"type":"object","properties":{"config":{"type":"array","items":{"$ref":"#/components/schemas/SystemConfigResponseItem"}}},"required":["config"]},"UpdateSystemConfigDto":{"type":"object","properties":{}}}}} \ No newline at end of file +{"openapi":"3.0.0","paths":{"/user":{"get":{"operationId":"getAllUsers","parameters":[{"name":"isAll","required":true,"in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}}}}}},"tags":["User"],"security":[{"bearer":[]}]},"post":{"operationId":"createUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]},"put":{"operationId":"updateUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/info/{userId}":{"get":{"operationId":"getUserById","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"]}},"/user/me":{"get":{"operationId":"getMyUserInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/count":{"get":{"operationId":"getUserCount","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserCountResponseDto"}}}}},"tags":["User"]}},"/user/{userId}":{"delete":{"operationId":"deleteUser","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/{userId}/restore":{"post":{"operationId":"restoreUser","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/profile-image":{"post":{"operationId":"createProfileImage","parameters":[],"requestBody":{"required":true,"description":"A new avatar for the user","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/CreateProfileImageDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProfileImageResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/profile-image/{userId}":{"get":{"operationId":"getProfileImage","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["User"]}},"/asset/upload":{"post":{"operationId":"uploadFile","parameters":[],"requestBody":{"required":true,"description":"Asset Upload Information","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/AssetFileUploadDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetFileUploadResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/download/{assetId}":{"get":{"operationId":"downloadFile","parameters":[{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}},{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/download-library":{"get":{"operationId":"downloadLibrary","parameters":[{"name":"skip","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/file/{assetId}":{"get":{"operationId":"serveFile","parameters":[{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}},{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/thumbnail/{assetId}":{"get":{"operationId":"getAssetThumbnail","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}},{"name":"format","required":false,"in":"query","schema":{"$ref":"#/components/schemas/ThumbnailFormat"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/curated-objects":{"get":{"operationId":"getCuratedObjects","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedObjectsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/curated-locations":{"get":{"operationId":"getCuratedLocations","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedLocationsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search-terms":{"get":{"operationId":"getAssetSearchTerms","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search":{"post":{"operationId":"searchAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchAssetDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/count-by-time-bucket":{"post":{"operationId":"getAssetCountByTimeBucket","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAssetCountByTimeBucketDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCountByTimeBucketResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/count-by-user-id":{"get":{"operationId":"getAssetCountByUserId","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCountByUserIdResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset":{"get":{"operationId":"getAllAssets","summary":"","description":"Get all AssetEntity belong to the user","parameters":[{"name":"if-none-match","in":"header","description":"ETag of data already cached on the client","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DeleteAssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/time-bucket":{"post":{"operationId":"getAssetByTimeBucket","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAssetByTimeBucketDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/{deviceId}":{"get":{"operationId":"getUserAssetsByDeviceId","summary":"","description":"Get all asset of a device that are in the database, ID only.","parameters":[{"name":"deviceId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/assetById/{assetId}":{"get":{"operationId":"getAssetById","summary":"","description":"Get a single asset's information","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]},"put":{"operationId":"updateAssetById","summary":"","description":"Update an asset","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/check":{"post":{"operationId":"checkDuplicateAsset","summary":"","description":"Check duplicated asset before uploading - for Web upload used","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/exist":{"post":{"operationId":"checkExistingAssets","summary":"","description":"Checks if multiple assets exist on the server and returns all existing - used by background backup","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckExistingAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckExistingAssetsResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/auth/login":{"post":{"operationId":"login","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginCredentialDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponseDto"}}}}},"tags":["Authentication"]}},"/auth/admin-sign-up":{"post":{"operationId":"adminSignUp","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignUpDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSignupResponseDto"}}}},"400":{"description":"The server already has an admin"}},"tags":["Authentication"]}},"/auth/validateToken":{"post":{"operationId":"validateAccessToken","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateAccessTokenResponseDto"}}}}},"tags":["Authentication"],"security":[{"bearer":[]}]}},"/auth/logout":{"post":{"operationId":"logout","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogoutResponseDto"}}}}},"tags":["Authentication"]}},"/oauth/config":{"post":{"operationId":"generateConfig","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthConfigDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthConfigResponseDto"}}}}},"tags":["OAuth"]}},"/oauth/callback":{"post":{"operationId":"callback","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthCallbackDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponseDto"}}}}},"tags":["OAuth"]}},"/device-info":{"post":{"operationId":"createDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDeviceInfoDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDeviceInfoDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]}},"/server-info":{"get":{"operationId":"getServerInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerInfoResponseDto"}}}}},"tags":["Server Info"]}},"/server-info/ping":{"get":{"operationId":"pingServer","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerPingResponse"}}}}},"tags":["Server Info"]}},"/server-info/version":{"get":{"operationId":"getServerVersion","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerVersionReponseDto"}}}}},"tags":["Server Info"]}},"/server-info/stats":{"get":{"operationId":"getStats","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerStatsResponseDto"}}}}},"tags":["Server Info"]}},"/album/count-by-user-id":{"get":{"operationId":"getAlbumCountByUserId","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumCountResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album":{"post":{"operationId":"createAlbum","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAlbumDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"get":{"operationId":"getAllAlbums","parameters":[{"name":"shared","required":false,"in":"query","schema":{"type":"boolean"}},{"name":"assetId","required":false,"in":"query","description":"Only returns albums that contain the asset\nIgnores the shared parameter\nundefined: get all albums","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/users":{"put":{"operationId":"addUsersToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddUsersDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/assets":{"put":{"operationId":"addAssetsToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddAssetsResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"removeAssetFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}":{"get":{"operationId":"getAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAlbumDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/user/{userId}":{"delete":{"operationId":"removeUserFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}},{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/download":{"get":{"operationId":"downloadArchive","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}},{"name":"skip","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/jobs":{"get":{"operationId":"getAllJobsStatus","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AllJobStatusResponseDto"}}}}},"tags":["Job"],"security":[{"bearer":[]}]}},"/jobs/{jobId}":{"get":{"operationId":"getJobStatus","parameters":[{"name":"jobId","required":true,"in":"path","schema":{"$ref":"#/components/schemas/JobId"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobStatusResponseDto"}}}}},"tags":["Job"],"security":[{"bearer":[]}]},"put":{"operationId":"sendJobCommand","parameters":[{"name":"jobId","required":true,"in":"path","schema":{"$ref":"#/components/schemas/JobId"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobCommandDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"number"}}}}},"tags":["Job"],"security":[{"bearer":[]}]}},"/system-config":{"get":{"operationId":"getConfig","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemConfigResponseDto"}}}}},"tags":["System Config"],"security":[{"bearer":[]}]},"put":{"operationId":"updateConfig","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSystemConfigDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemConfigResponseDto"}}}}},"tags":["System Config"],"security":[{"bearer":[]}]}}},"info":{"title":"Immich","description":"Immich API","version":"1.17.0","contact":{}},"tags":[],"servers":[{"url":"/api"}],"components":{"securitySchemes":{"bearer":{"scheme":"Bearer","bearerFormat":"JWT","type":"http","name":"JWT","description":"Enter JWT token","in":"header"}},"schemas":{"UserResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"},"profileImagePath":{"type":"string"},"shouldChangePassword":{"type":"boolean"},"isAdmin":{"type":"boolean"},"deletedAt":{"format":"date-time","type":"string"}},"required":["id","email","firstName","lastName","createdAt","profileImagePath","shouldChangePassword","isAdmin"]},"CreateUserDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"John"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"UserCountResponseDto":{"type":"object","properties":{"userCount":{"type":"integer"}},"required":["userCount"]},"UpdateUserDto":{"type":"object","properties":{"id":{"type":"string"},"password":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"isAdmin":{"type":"boolean"},"shouldChangePassword":{"type":"boolean"},"profileImagePath":{"type":"string"}},"required":["id"]},"CreateProfileImageDto":{"type":"object","properties":{"file":{"type":"string","format":"binary"}},"required":["file"]},"CreateProfileImageResponseDto":{"type":"object","properties":{"userId":{"type":"string"},"profileImagePath":{"type":"string"}},"required":["userId","profileImagePath"]},"AssetFileUploadDto":{"type":"object","properties":{"assetData":{"type":"string","format":"binary"}},"required":["assetData"]},"AssetFileUploadResponseDto":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]},"ThumbnailFormat":{"type":"string","enum":["JPEG","WEBP"]},"CuratedObjectsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","object","resizePath","deviceAssetId","deviceId"]},"CuratedLocationsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"city":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","city","resizePath","deviceAssetId","deviceId"]},"SearchAssetDto":{"type":"object","properties":{"searchTerm":{"type":"string"}},"required":["searchTerm"]},"AssetTypeEnum":{"type":"string","enum":["IMAGE","VIDEO","AUDIO","OTHER"]},"ExifResponseDto":{"type":"object","properties":{"id":{"type":"integer","nullable":true,"default":null,"format":"int64"},"fileSizeInByte":{"type":"integer","nullable":true,"default":null,"format":"int64"},"make":{"type":"string","nullable":true,"default":null},"model":{"type":"string","nullable":true,"default":null},"imageName":{"type":"string","nullable":true,"default":null},"exifImageWidth":{"type":"number","nullable":true,"default":null},"exifImageHeight":{"type":"number","nullable":true,"default":null},"orientation":{"type":"string","nullable":true,"default":null},"dateTimeOriginal":{"format":"date-time","type":"string","nullable":true,"default":null},"modifyDate":{"format":"date-time","type":"string","nullable":true,"default":null},"lensModel":{"type":"string","nullable":true,"default":null},"fNumber":{"type":"number","nullable":true,"default":null},"focalLength":{"type":"number","nullable":true,"default":null},"iso":{"type":"number","nullable":true,"default":null},"exposureTime":{"type":"number","nullable":true,"default":null},"latitude":{"type":"number","nullable":true,"default":null},"longitude":{"type":"number","nullable":true,"default":null},"city":{"type":"string","nullable":true,"default":null},"state":{"type":"string","nullable":true,"default":null},"country":{"type":"string","nullable":true,"default":null}}},"SmartInfoResponseDto":{"type":"object","properties":{"id":{"type":"string"},"tags":{"nullable":true,"type":"array","items":{"type":"string"}},"objects":{"nullable":true,"type":"array","items":{"type":"string"}}}},"AssetResponseDto":{"type":"object","properties":{"type":{"$ref":"#/components/schemas/AssetTypeEnum"},"id":{"type":"string"},"deviceAssetId":{"type":"string"},"ownerId":{"type":"string"},"deviceId":{"type":"string"},"originalPath":{"type":"string"},"resizePath":{"type":"string","nullable":true},"createdAt":{"type":"string"},"modifiedAt":{"type":"string"},"isFavorite":{"type":"boolean"},"mimeType":{"type":"string","nullable":true},"duration":{"type":"string"},"webpPath":{"type":"string","nullable":true},"encodedVideoPath":{"type":"string","nullable":true},"exifInfo":{"$ref":"#/components/schemas/ExifResponseDto"},"smartInfo":{"$ref":"#/components/schemas/SmartInfoResponseDto"},"livePhotoVideoId":{"type":"string","nullable":true}},"required":["type","id","deviceAssetId","ownerId","deviceId","originalPath","resizePath","createdAt","modifiedAt","isFavorite","mimeType","duration","webpPath"]},"TimeGroupEnum":{"type":"string","enum":["day","month"]},"GetAssetCountByTimeBucketDto":{"type":"object","properties":{"timeGroup":{"$ref":"#/components/schemas/TimeGroupEnum"}},"required":["timeGroup"]},"AssetCountByTimeBucket":{"type":"object","properties":{"timeBucket":{"type":"string"},"count":{"type":"integer"}},"required":["timeBucket","count"]},"AssetCountByTimeBucketResponseDto":{"type":"object","properties":{"totalCount":{"type":"integer"},"buckets":{"type":"array","items":{"$ref":"#/components/schemas/AssetCountByTimeBucket"}}},"required":["totalCount","buckets"]},"AssetCountByUserIdResponseDto":{"type":"object","properties":{"audio":{"type":"integer","default":0},"photos":{"type":"integer","default":0},"videos":{"type":"integer","default":0},"other":{"type":"integer","default":0},"total":{"type":"integer","default":0}},"required":["audio","photos","videos","other","total"]},"GetAssetByTimeBucketDto":{"type":"object","properties":{"timeBucket":{"title":"Array of date time buckets","example":["2015-06-01T00:00:00.000Z","2016-02-01T00:00:00.000Z","2016-03-01T00:00:00.000Z"],"type":"array","items":{"type":"string"}}},"required":["timeBucket"]},"UpdateAssetDto":{"type":"object","properties":{"isFavorite":{"type":"boolean"}},"required":["isFavorite"]},"DeleteAssetDto":{"type":"object","properties":{"ids":{"title":"Array of asset IDs to delete","example":["bf973405-3f2a-48d2-a687-2ed4167164be","dd41870b-5d00-46d2-924e-1d8489a0aa0f","fad77c3f-deef-4e7e-9608-14c1aa4e559a"],"type":"array","items":{"type":"string"}}},"required":["ids"]},"DeleteAssetStatus":{"type":"string","enum":["SUCCESS","FAILED"]},"DeleteAssetResponseDto":{"type":"object","properties":{"status":{"$ref":"#/components/schemas/DeleteAssetStatus"},"id":{"type":"string"}},"required":["status","id"]},"CheckDuplicateAssetDto":{"type":"object","properties":{"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["deviceAssetId","deviceId"]},"CheckDuplicateAssetResponseDto":{"type":"object","properties":{"isExist":{"type":"boolean"},"id":{"type":"string"}},"required":["isExist"]},"CheckExistingAssetsDto":{"type":"object","properties":{"deviceAssetIds":{"type":"array","items":{"type":"string"}},"deviceId":{"type":"string"}},"required":["deviceAssetIds","deviceId"]},"CheckExistingAssetsResponseDto":{"type":"object","properties":{"existingIds":{"type":"array","items":{"type":"string"}}},"required":["existingIds"]},"LoginCredentialDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"}},"required":["email","password"]},"LoginResponseDto":{"type":"object","properties":{"accessToken":{"type":"string","readOnly":true},"userId":{"type":"string","readOnly":true},"userEmail":{"type":"string","readOnly":true},"firstName":{"type":"string","readOnly":true},"lastName":{"type":"string","readOnly":true},"profileImagePath":{"type":"string","readOnly":true},"isAdmin":{"type":"boolean","readOnly":true},"shouldChangePassword":{"type":"boolean","readOnly":true}},"required":["accessToken","userId","userEmail","firstName","lastName","profileImagePath","isAdmin","shouldChangePassword"]},"SignUpDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"Admin"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"AdminSignupResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"}},"required":["id","email","firstName","lastName","createdAt"]},"ValidateAccessTokenResponseDto":{"type":"object","properties":{"authStatus":{"type":"boolean"}},"required":["authStatus"]},"LogoutResponseDto":{"type":"object","properties":{"successful":{"type":"boolean","readOnly":true},"redirectUri":{"type":"string","readOnly":true}},"required":["successful","redirectUri"]},"OAuthConfigDto":{"type":"object","properties":{"redirectUri":{"type":"string"}},"required":["redirectUri"]},"OAuthConfigResponseDto":{"type":"object","properties":{"enabled":{"type":"boolean","readOnly":true},"url":{"type":"string","readOnly":true},"buttonText":{"type":"string","readOnly":true}},"required":["enabled"]},"OAuthCallbackDto":{"type":"object","properties":{"url":{"type":"string"}},"required":["url"]},"DeviceTypeEnum":{"type":"string","enum":["IOS","ANDROID","WEB"]},"CreateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"DeviceInfoResponseDto":{"type":"object","properties":{"id":{"type":"integer"},"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"userId":{"type":"string"},"deviceId":{"type":"string"},"createdAt":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["id","deviceType","userId","deviceId","createdAt","isAutoBackup"]},"UpdateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"ServerInfoResponseDto":{"type":"object","properties":{"diskSizeRaw":{"type":"integer","format":"int64"},"diskUseRaw":{"type":"integer","format":"int64"},"diskAvailableRaw":{"type":"integer","format":"int64"},"diskUsagePercentage":{"type":"number","format":"float"},"diskSize":{"type":"string"},"diskUse":{"type":"string"},"diskAvailable":{"type":"string"}},"required":["diskSizeRaw","diskUseRaw","diskAvailableRaw","diskUsagePercentage","diskSize","diskUse","diskAvailable"]},"ServerPingResponse":{"type":"object","properties":{"res":{"type":"string","readOnly":true,"example":"pong"}},"required":["res"]},"ServerVersionReponseDto":{"type":"object","properties":{"major":{"type":"integer"},"minor":{"type":"integer"},"patch":{"type":"integer"},"build":{"type":"integer"}},"required":["major","minor","patch","build"]},"UsageByUserDto":{"type":"object","properties":{"userId":{"type":"string"},"videos":{"type":"integer"},"photos":{"type":"integer"},"usageRaw":{"type":"integer","format":"int64"},"usage":{"type":"string"}},"required":["userId","videos","photos","usageRaw","usage"]},"ServerStatsResponseDto":{"type":"object","properties":{"photos":{"type":"integer"},"videos":{"type":"integer"},"objects":{"type":"integer"},"usageRaw":{"type":"integer","format":"int64"},"usage":{"type":"string"},"usageByUser":{"title":"Array of usage for each user","example":[{"photos":1,"videos":1,"diskUsageRaw":1}],"type":"array","items":{"$ref":"#/components/schemas/UsageByUserDto"}}},"required":["photos","videos","objects","usageRaw","usage","usageByUser"]},"AlbumCountResponseDto":{"type":"object","properties":{"owned":{"type":"integer"},"shared":{"type":"integer"},"sharing":{"type":"integer"}},"required":["owned","shared","sharing"]},"CreateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"sharedWithUserIds":{"type":"array","items":{"type":"string"}},"assetIds":{"type":"array","items":{"type":"string"}}},"required":["albumName"]},"AlbumResponseDto":{"type":"object","properties":{"assetCount":{"type":"integer"},"id":{"type":"string"},"ownerId":{"type":"string"},"albumName":{"type":"string"},"createdAt":{"type":"string"},"albumThumbnailAssetId":{"type":"string","nullable":true},"shared":{"type":"boolean"},"sharedUsers":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}},"assets":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}},"required":["assetCount","id","ownerId","albumName","createdAt","albumThumbnailAssetId","shared","sharedUsers","assets"]},"AddUsersDto":{"type":"object","properties":{"sharedUserIds":{"type":"array","items":{"type":"string"}}},"required":["sharedUserIds"]},"AddAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"AddAssetsResponseDto":{"type":"object","properties":{"successfullyAdded":{"type":"integer"},"alreadyInAlbum":{"type":"array","items":{"type":"string"}},"album":{"$ref":"#/components/schemas/AlbumResponseDto"}},"required":["successfullyAdded","alreadyInAlbum"]},"RemoveAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"UpdateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"albumThumbnailAssetId":{"type":"string"}}},"JobCounts":{"type":"object","properties":{"active":{"type":"integer"},"completed":{"type":"integer"},"failed":{"type":"integer"},"delayed":{"type":"integer"},"waiting":{"type":"integer"}},"required":["active","completed","failed","delayed","waiting"]},"AllJobStatusResponseDto":{"type":"object","properties":{"thumbnailGenerationQueueCount":{"$ref":"#/components/schemas/JobCounts"},"metadataExtractionQueueCount":{"$ref":"#/components/schemas/JobCounts"},"videoConversionQueueCount":{"$ref":"#/components/schemas/JobCounts"},"machineLearningQueueCount":{"$ref":"#/components/schemas/JobCounts"},"isThumbnailGenerationActive":{"type":"boolean"},"isMetadataExtractionActive":{"type":"boolean"},"isVideoConversionActive":{"type":"boolean"},"isMachineLearningActive":{"type":"boolean"}},"required":["thumbnailGenerationQueueCount","metadataExtractionQueueCount","videoConversionQueueCount","machineLearningQueueCount","isThumbnailGenerationActive","isMetadataExtractionActive","isVideoConversionActive","isMachineLearningActive"]},"JobId":{"type":"string","enum":["thumbnail-generation","metadata-extraction","video-conversion","machine-learning"]},"JobStatusResponseDto":{"type":"object","properties":{"isActive":{"type":"boolean"},"queueCount":{"type":"object"}},"required":["isActive","queueCount"]},"JobCommand":{"type":"string","enum":["start","stop"]},"JobCommandDto":{"type":"object","properties":{"command":{"$ref":"#/components/schemas/JobCommand"}},"required":["command"]},"SystemConfigKey":{"type":"string","enum":["ffmpeg_crf","ffmpeg_preset","ffmpeg_target_video_codec","ffmpeg_target_audio_codec","ffmpeg_target_scaling"]},"SystemConfigResponseItem":{"type":"object","properties":{"name":{"type":"string"},"key":{"$ref":"#/components/schemas/SystemConfigKey"},"value":{"type":"string"},"defaultValue":{"type":"string"}},"required":["name","key","value","defaultValue"]},"SystemConfigResponseDto":{"type":"object","properties":{"config":{"type":"array","items":{"$ref":"#/components/schemas/SystemConfigResponseItem"}}},"required":["config"]},"UpdateSystemConfigDto":{"type":"object","properties":{}}}}} \ No newline at end of file From d31eddf32fc83df5d6d913f66685133023e70295 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 30 Nov 2022 10:58:07 -0600 Subject: [PATCH 28/30] chore(mobile) Improve mobile UI (#1038) --- mobile/assets/i18n/en-US.json | 2 +- .../album/ui/album_thumbnail_card.dart | 49 ++++++++---- .../backup/providers/backup.provider.dart | 35 +++++--- .../backup/views/album_preview_page.dart | 2 +- .../views/backup_album_selection_page.dart | 75 ++++++++++++++---- .../modules/home/services/asset.service.dart | 21 +++-- .../modules/home/ui/immich_sliver_appbar.dart | 4 +- mobile/lib/modules/home/views/home_page.dart | 68 +++++++++++++--- mobile/lib/modules/login/ui/login_form.dart | 18 ++++- .../shared/ui/immich_loading_indicator.dart | 5 +- .../openapi/lib/model/album_response_dto.dart | Bin 5619 -> 5774 bytes 11 files changed, 214 insertions(+), 65 deletions(-) diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 0adacb6494..5e072b84d4 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -17,7 +17,7 @@ "backup_album_selection_page_albums_device": "Albums on device ({})", "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude", "backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.", - "backup_album_selection_page_select_albums": "Select Albums", + "backup_album_selection_page_select_albums": "Select albums", "backup_album_selection_page_selection_info": "Selection Info", "backup_album_selection_page_total_assets": "Total unique assets", "backup_all": "All", diff --git a/mobile/lib/modules/album/ui/album_thumbnail_card.dart b/mobile/lib/modules/album/ui/album_thumbnail_card.dart index 5530b37ddb..87d2f45d59 100644 --- a/mobile/lib/modules/album/ui/album_thumbnail_card.dart +++ b/mobile/lib/modules/album/ui/album_thumbnail_card.dart @@ -21,8 +21,39 @@ class AlbumThumbnailCard extends StatelessWidget { @override Widget build(BuildContext context) { var box = Hive.box(userInfoBox); + var cardSize = MediaQuery.of(context).size.width / 2 - 18; + var isDarkMode = Theme.of(context).brightness == Brightness.dark; - final cardSize = MediaQuery.of(context).size.width / 2 - 18; + buildEmptyThumbnail() { + return Container( + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[800] : Colors.grey[200], + ), + child: SizedBox( + height: cardSize, + width: cardSize, + child: const Center( + child: Icon(Icons.no_photography), + ), + ), + ); + } + + buildAlbumThumbnail() { + return CachedNetworkImage( + memCacheHeight: max(400, cardSize.toInt() * 3), + width: cardSize, + height: cardSize, + fit: BoxFit.cover, + fadeInDuration: const Duration(milliseconds: 200), + imageUrl: getAlbumThumbnailUrl( + album, + type: ThumbnailFormat.JPEG, + ), + httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"}, + cacheKey: "${album.albumThumbnailAssetId}", + ); + } return GestureDetector( onTap: () { @@ -35,19 +66,9 @@ class AlbumThumbnailCard extends StatelessWidget { children: [ ClipRRect( borderRadius: BorderRadius.circular(8), - child: CachedNetworkImage( - memCacheHeight: max(400, cardSize.toInt() * 3), - width: cardSize, - height: cardSize, - fit: BoxFit.cover, - fadeInDuration: const Duration(milliseconds: 200), - imageUrl: - getAlbumThumbnailUrl(album, type: ThumbnailFormat.JPEG), - httpHeaders: { - "Authorization": "Bearer ${box.get(accessTokenKey)}" - }, - cacheKey: "${album.albumThumbnailAssetId}", - ), + child: album.albumThumbnailAssetId == null + ? buildEmptyThumbnail() + : buildAlbumThumbnail(), ), Padding( padding: const EdgeInsets.only(top: 8.0), diff --git a/mobile/lib/modules/backup/providers/backup.provider.dart b/mobile/lib/modules/backup/providers/backup.provider.dart index 65f0eba624..0b3a61cc4c 100644 --- a/mobile/lib/modules/backup/providers/backup.provider.dart +++ b/mobile/lib/modules/backup/providers/backup.provider.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:cancellation_token_http/http.dart'; +import 'package:flutter/widgets.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/hive_box.dart'; @@ -68,6 +69,7 @@ class BackupNotifier extends StateNotifier { final AuthenticationState _authState; final BackgroundService _backgroundService; final Ref ref; + var isGettingBackupInfo = false; /// /// UI INTERACTION @@ -172,9 +174,10 @@ class BackupNotifier extends StateNotifier { /// Get all album on the device /// Get all selected and excluded album from the user's persistent storage /// If this is the first time performing backup - set the default selected album to be - /// the one that has all assets (Recent on Android, Recents on iOS) + /// the one that has all assets (`Recent` on Android, `Recents` on iOS) /// Future _getBackupAlbumsInfo() async { + Stopwatch stopwatch = Stopwatch()..start(); // Get all albums on the device List availableAlbums = []; List albums = await PhotoManager.getAssetPathList( @@ -182,6 +185,8 @@ class BackupNotifier extends StateNotifier { type: RequestType.common, ); + log.info('Found ${albums.length} local albums'); + for (AssetPathEntity album in albums) { AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album); @@ -293,6 +298,8 @@ class BackupNotifier extends StateNotifier { } catch (e, stackTrace) { log.severe("Failed to generate album from id", e, stackTrace); } + + debugPrint("_getBackupAlbumsInfo takes ${stopwatch.elapsedMilliseconds}ms"); } /// @@ -364,25 +371,29 @@ class BackupNotifier extends StateNotifier { return; } - /// /// Get all necessary information for calculating the available albums, /// which albums are selected or excluded /// and then update the UI according to those information - /// Future getBackupInfo() async { - final bool isEnabled = await _backgroundService.isBackgroundBackupEnabled(); - state = state.copyWith(backgroundBackup: isEnabled); - if (state.backupProgress != BackUpProgressEnum.inBackground) { - await _getBackupAlbumsInfo(); - await _updateServerInfo(); - await _updateBackupAssetCount(); + if (!isGettingBackupInfo) { + isGettingBackupInfo = true; + + var isEnabled = await _backgroundService.isBackgroundBackupEnabled(); + + state = state.copyWith(backgroundBackup: isEnabled); + + if (state.backupProgress != BackUpProgressEnum.inBackground) { + await _getBackupAlbumsInfo(); + await _updateServerInfo(); + await _updateBackupAssetCount(); + } + + isGettingBackupInfo = false; } } - /// /// Save user selection of selected albums and excluded albums to /// Hive database - /// void _updatePersistentAlbumsSelection() { final epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true); Box backupAlbumInfoBox = @@ -402,9 +413,7 @@ class BackupNotifier extends StateNotifier { ); } - /// /// Invoke backup process - /// Future startBackupProcess() async { assert(state.backupProgress == BackUpProgressEnum.idle); state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress); diff --git a/mobile/lib/modules/backup/views/album_preview_page.dart b/mobile/lib/modules/backup/views/album_preview_page.dart index c236934794..27ca79082b 100644 --- a/mobile/lib/modules/backup/views/album_preview_page.dart +++ b/mobile/lib/modules/backup/views/album_preview_page.dart @@ -36,7 +36,7 @@ class AlbumPreviewPage extends HookConsumerWidget { title: Column( children: [ Text( - "${album.name} (${album.assetCountAsync})", + album.name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), ), Padding( diff --git a/mobile/lib/modules/backup/views/backup_album_selection_page.dart b/mobile/lib/modules/backup/views/backup_album_selection_page.dart index c94552ca3f..3de7294742 100644 --- a/mobile/lib/modules/backup/views/backup_album_selection_page.dart +++ b/mobile/lib/modules/backup/views/backup_album_selection_page.dart @@ -5,6 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/immich_colors.dart'; +import 'package:immich_mobile/modules/backup/models/available_album.model.dart'; import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/modules/backup/ui/album_info_card.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; @@ -14,10 +15,13 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { const BackupAlbumSelectionPage({Key? key}) : super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { - final availableAlbums = ref.watch(backupProvider).availableAlbums; + // final availableAlbums = ref.watch(backupProvider).availableAlbums; final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums; final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums; final isDarkTheme = Theme.of(context).brightness == Brightness.dark; + final albums = useState>( + ref.watch(backupProvider).availableAlbums, + ); useEffect( () { @@ -28,7 +32,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { ); buildAlbumSelectionList() { - if (availableAlbums.isEmpty) { + if (albums.value.isEmpty) { return const Center( child: ImmichLoadingIndicator(), ); @@ -38,17 +42,17 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { height: 265, child: ListView.builder( scrollDirection: Axis.horizontal, - itemCount: availableAlbums.length, + itemCount: albums.value.length, physics: const BouncingScrollPhysics(), itemBuilder: ((context, index) { - var thumbnailData = availableAlbums[index].thumbnailData; + var thumbnailData = albums.value[index].thumbnailData; return Padding( padding: index == 0 ? const EdgeInsets.only(left: 16.00) : const EdgeInsets.all(0), child: AlbumInfoCard( imageData: thumbnailData, - albumInfo: availableAlbums[index], + albumInfo: albums.value[index], ), ); }), @@ -79,15 +83,13 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { child: Chip( visualDensity: VisualDensity.compact, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5), + borderRadius: BorderRadius.circular(10), ), label: Text( album.name, style: TextStyle( fontSize: 10, - color: Theme.of(context).brightness == Brightness.dark - ? Colors.black - : Colors.white, + color: isDarkTheme ? Colors.black : Colors.white, fontWeight: FontWeight.bold, ), ), @@ -119,7 +121,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { child: Chip( visualDensity: VisualDensity.compact, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5), + borderRadius: BorderRadius.circular(10), ), label: Text( album.name, @@ -143,6 +145,46 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { }).toSet(); } + buildSearchBar() { + return Padding( + padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 8.0), + child: TextFormField( + onChanged: (searchValue) { + albums.value = ref + .watch(backupProvider) + .availableAlbums + .where( + (album) => album.name + .toLowerCase() + .contains(searchValue.toLowerCase()), + ) + .toList(); + }, + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 8.0, + ), + hintText: "Search", + hintStyle: TextStyle( + color: isDarkTheme ? Colors.white : Colors.grey, + fontSize: 14.0, + ), + prefixIcon: const Icon( + Icons.search, + color: Colors.grey, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide.none, + ), + filled: true, + fillColor: isDarkTheme ? Colors.white30 : Colors.grey[200], + ), + ), + ); + } + return Scaffold( appBar: AppBar( leading: IconButton( @@ -188,7 +230,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { child: Card( margin: const EdgeInsets.all(0), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5), + borderRadius: BorderRadius.circular(10), side: BorderSide( color: isDarkTheme ? const Color.fromARGB(255, 0, 0, 0) @@ -225,8 +267,11 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { ListTile( title: Text( - "backup_album_selection_page_albums_device" - .tr(args: [availableAlbums.length.toString()]), + "backup_album_selection_page_albums_device".tr( + args: [ + ref.watch(backupProvider).availableAlbums.length.toString() + ], + ), style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14), ), subtitle: Padding( @@ -254,7 +299,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { builder: (BuildContext context) { return AlertDialog( shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(10), ), elevation: 5, title: Text( @@ -284,6 +329,8 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { ), ), + buildSearchBar(), + Padding( padding: const EdgeInsets.only(bottom: 16.0), child: buildAlbumSelectionList(), diff --git a/mobile/lib/modules/home/services/asset.service.dart b/mobile/lib/modules/home/services/asset.service.dart index 2d8014484d..0599d0c547 100644 --- a/mobile/lib/modules/home/services/asset.service.dart +++ b/mobile/lib/modules/home/services/asset.service.dart @@ -12,6 +12,7 @@ import 'package:immich_mobile/shared/providers/api.provider.dart'; import 'package:immich_mobile/shared/services/api.service.dart'; import 'package:immich_mobile/utils/openapi_extensions.dart'; import 'package:immich_mobile/utils/tuple.dart'; +import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; final assetServiceProvider = Provider( @@ -26,20 +27,26 @@ class AssetService { final ApiService _apiService; final BackupService _backupService; final BackgroundService _backgroundService; + final log = Logger('AssetService'); AssetService(this._apiService, this._backupService, this._backgroundService); /// Returns `null` if the server state did not change, else list of assets Future?> getRemoteAssets({required bool hasCache}) async { - final Box box = Hive.box(userInfoBox); - final Pair, String?>? remote = await _apiService - .assetApi - .getAllAssetsWithETag(eTag: hasCache ? box.get(assetEtagKey) : null); - if (remote == null) { + try { + final Box box = Hive.box(userInfoBox); + final Pair, String?>? remote = await _apiService + .assetApi + .getAllAssetsWithETag(eTag: hasCache ? box.get(assetEtagKey) : null); + if (remote == null) { + return null; + } + box.put(assetEtagKey, remote.second); + return remote.first.map(Asset.remote).toList(growable: false); + } catch (e, stack) { + log.severe('Error while getting remote assets', e, stack); return null; } - box.put(assetEtagKey, remote.second); - return remote.first.map(Asset.remote).toList(growable: false); } /// if [urgent] is `true`, do not block by waiting on the background service diff --git a/mobile/lib/modules/home/ui/immich_sliver_appbar.dart b/mobile/lib/modules/home/ui/immich_sliver_appbar.dart index 63761c5783..aeac61c9dc 100644 --- a/mobile/lib/modules/home/ui/immich_sliver_appbar.dart +++ b/mobile/lib/modules/home/ui/immich_sliver_appbar.dart @@ -32,7 +32,9 @@ class ImmichSliverAppBar extends ConsumerWidget { snap: false, backgroundColor: Theme.of(context).appBarTheme.backgroundColor, shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(5)), + borderRadius: BorderRadius.all( + Radius.circular(5), + ), ), leading: Builder( builder: (BuildContext context) { diff --git a/mobile/lib/modules/home/views/home_page.dart b/mobile/lib/modules/home/views/home_page.dart index 5b2fbbb416..1fafc63436 100644 --- a/mobile/lib/modules/home/views/home_page.dart +++ b/mobile/lib/modules/home/views/home_page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -20,6 +22,7 @@ import 'package:immich_mobile/shared/providers/asset.provider.dart'; import 'package:immich_mobile/shared/providers/server_info.provider.dart'; import 'package:immich_mobile/shared/providers/websocket.provider.dart'; import 'package:immich_mobile/shared/services/share.service.dart'; +import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/immich_toast.dart'; import 'package:openapi/api.dart'; @@ -37,6 +40,8 @@ class HomePage extends HookConsumerWidget { final albums = ref.watch(albumProvider); final albumService = ref.watch(albumServiceProvider); + final tipOneOpacity = useState(0.0); + useEffect( () { ref.read(websocketProvider.notifier).connect(); @@ -146,6 +151,49 @@ class HomePage extends HookConsumerWidget { } } + buildLoadingIndicator() { + Timer(const Duration(seconds: 2), () { + tipOneOpacity.value = 1; + }); + + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const ImmichLoadingIndicator(), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Text( + 'Building the timeline', + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + color: Theme.of(context).primaryColor, + ), + ), + ), + AnimatedOpacity( + duration: const Duration(milliseconds: 500), + opacity: tipOneOpacity.value, + child: const SizedBox( + width: 250, + child: Padding( + padding: EdgeInsets.only(top: 8.0), + child: Text( + 'If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).', + textAlign: TextAlign.justify, + style: TextStyle( + fontSize: 12, + ), + ), + ), + ), + ) + ], + ), + ); + } + return SafeArea( bottom: !multiselectEnabled.state, top: true, @@ -164,15 +212,17 @@ class HomePage extends HookConsumerWidget { top: selectionEnabledHook.value ? 0 : 60, bottom: 0.0, ), - child: ImmichAssetGrid( - renderList: renderList, - assetsPerRow: - appSettingService.getSetting(AppSettingsEnum.tilesPerRow), - showStorageIndicator: appSettingService - .getSetting(AppSettingsEnum.storageIndicator), - listener: selectionListener, - selectionActive: selectionEnabledHook.value, - ), + child: ref.watch(assetProvider).isEmpty + ? buildLoadingIndicator() + : ImmichAssetGrid( + renderList: renderList, + assetsPerRow: appSettingService + .getSetting(AppSettingsEnum.tilesPerRow), + showStorageIndicator: appSettingService + .getSetting(AppSettingsEnum.storageIndicator), + listener: selectionListener, + selectionActive: selectionEnabledHook.value, + ), ), if (selectionEnabledHook.value) ControlBottomAppBar( diff --git a/mobile/lib/modules/login/ui/login_form.dart b/mobile/lib/modules/login/ui/login_form.dart index 6a0b72a0b8..13f53b8ff1 100644 --- a/mobile/lib/modules/login/ui/login_form.dart +++ b/mobile/lib/modules/login/ui/login_form.dart @@ -83,6 +83,13 @@ class LoginForm extends HookConsumerWidget { [], ); + populateTestLoginInfo() { + usernameController.text = 'testuser@email.com'; + passwordController.text = 'password'; + serverEndpointController.text = 'http://10.1.15.216:2283/api'; + isSaveLoginInfo.value = true; + } + return Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 300), @@ -92,10 +99,13 @@ class LoginForm extends HookConsumerWidget { runSpacing: 16, alignment: WrapAlignment.center, children: [ - const Image( - image: AssetImage('assets/immich-logo-no-outline.png'), - width: 100, - filterQuality: FilterQuality.high, + GestureDetector( + onDoubleTap: () => populateTestLoginInfo(), + child: const Image( + image: AssetImage('assets/immich-logo-no-outline.png'), + width: 100, + filterQuality: FilterQuality.high, + ), ), Text( 'IMMICH', diff --git a/mobile/lib/shared/ui/immich_loading_indicator.dart b/mobile/lib/shared/ui/immich_loading_indicator.dart index 84a33ba046..bd4ad0d3cb 100644 --- a/mobile/lib/shared/ui/immich_loading_indicator.dart +++ b/mobile/lib/shared/ui/immich_loading_indicator.dart @@ -15,7 +15,10 @@ class ImmichLoadingIndicator extends StatelessWidget { borderRadius: BorderRadius.circular(10), ), padding: const EdgeInsets.all(15), - child: const CircularProgressIndicator(color: Colors.white), + child: const CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), ); } } diff --git a/mobile/openapi/lib/model/album_response_dto.dart b/mobile/openapi/lib/model/album_response_dto.dart index ac170523d856a423cf67a4cfc0a2e12cf137fd2d..f9901a358f593e78e93c30636b48f1eb7ba6d872 100644 GIT binary patch delta 483 zcmeyY-KV=@InP-xWTX~pD%8|KFxaoF;E&QJH*&MQ`$77MIEPteTV4Smh=+ zv5HA5C}`xD=cN{Trs!oP7H2r;r=)5sXajAUyp~md@+nqXW}u46{OqEW|FW7*He{2U z?8zoSIgQPN4dR;3^Vl>QC!b_jpZtkkd@?tO;$%Gz)ybh8=O(Li%1?IV)ZARislgO6NQh%)EN)C8(jP^i_khN}T`i&9HUi}Dmo zib_)v;+qff6fyEZm4U-zvLUa+<{&`&DA31i~w6iexLvV delta 335 zcmeCv{j9xVI`ibY%u4QMwJfS^`6U^tMS7D3I7KHP zVo{p>l0|+pFRSKcYgYToHLU8Bm$J%CKFlg2s*zuwms;eRqL-0aoZ+0GlB%ho%{BQ0 ztNdg^Hfcr;uznXdv&l7V5|SFl8Hq)yDWS!wMa8JY}uI9Y;2bFw{$XYYiT$r52sW`clQ+x9!PAw+E#A2ZC5)BPag=#JZ5SaX#TWzuoPsZeSo(ygU zg<4H(i1;L4vCWdag^ZIA@G5NH#;4E7pHi8Zn46ibkX4+Yr=w6k`5(X7W^sXLW~>rt ogn1biQu9iRD)q_|b4pWn6w-_G%M+7wQmx=Doz3E+<&2D60Bk>K^8f$< From a3847987798703f547955698b7f64ec8c8650c1d Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 30 Nov 2022 11:18:06 -0600 Subject: [PATCH 29/30] Up version for release --- mobile/android/fastlane/Fastfile | 4 ++-- .../fastlane/metadata/android/en-US/changelogs/58.txt | 6 ++++++ mobile/ios/fastlane/Fastfile | 2 +- mobile/pubspec.yaml | 2 +- server/apps/immich/src/constants/server_version.constant.ts | 6 +++--- 5 files changed, 13 insertions(+), 7 deletions(-) create mode 100644 mobile/android/fastlane/metadata/android/en-US/changelogs/58.txt diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 768f3a3378..188807cc37 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 57, - "android.injected.version.name" => "1.36.2", + "android.injected.version.code" => 58, + "android.injected.version.name" => "1.37.0", } ) upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') diff --git a/mobile/android/fastlane/metadata/android/en-US/changelogs/58.txt b/mobile/android/fastlane/metadata/android/en-US/changelogs/58.txt new file mode 100644 index 0000000000..ba269eb4fd --- /dev/null +++ b/mobile/android/fastlane/metadata/android/en-US/changelogs/58.txt @@ -0,0 +1,6 @@ +* Use binary prefixes for data sizes +* Fix not able to show device asset on Android 13 +* Use cached asset info if unchanged instead of downloading all assets +* Add in-app logging +* Add search mechanism to album selection page +* Improve UI \ No newline at end of file diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 7dc179e228..66600efe51 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Beta" lane :beta do increment_version_number( - version_number: "1.36.1" + version_number: "1.37.0" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index c479c27c7e..11f2f5ae35 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: "none" -version: 1.36.2+57 +version: 1.37.0+58 environment: sdk: ">=2.17.0 <3.0.0" diff --git a/server/apps/immich/src/constants/server_version.constant.ts b/server/apps/immich/src/constants/server_version.constant.ts index 219156c8cb..8b7e5a2d54 100644 --- a/server/apps/immich/src/constants/server_version.constant.ts +++ b/server/apps/immich/src/constants/server_version.constant.ts @@ -10,7 +10,7 @@ export interface IServerVersion { export const serverVersion: IServerVersion = { major: 1, - minor: 36, - patch: 2, - build: 56, + minor: 37, + patch: 0, + build: 58, }; From a3971543b58208194d01c692386e444fcd2bf27a Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 1 Dec 2022 09:20:53 -0600 Subject: [PATCH 30/30] fix(mobile): Start up from splash screen does not trigger foreground backup (#1042) --- mobile/android/fastlane/report.xml | 6 +++--- mobile/ios/Runner.xcodeproj/project.pbxproj | 6 +++--- mobile/ios/Runner/Info.plist | 4 ++-- mobile/ios/fastlane/report.xml | 12 +++++------ .../backup/providers/backup.provider.dart | 21 +++++++------------ 5 files changed, 21 insertions(+), 28 deletions(-) diff --git a/mobile/android/fastlane/report.xml b/mobile/android/fastlane/report.xml index da68f11b20..52da1b7299 100644 --- a/mobile/android/fastlane/report.xml +++ b/mobile/android/fastlane/report.xml @@ -5,17 +5,17 @@ - + - + - + diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index d546a2daed..95c7de92f2 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -360,7 +360,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 72; + CURRENT_PROJECT_VERSION = 73; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -495,7 +495,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 72; + CURRENT_PROJECT_VERSION = 73; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -522,7 +522,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 72; + CURRENT_PROJECT_VERSION = 73; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index cacb14f32a..73f4b0779e 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -17,11 +17,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.36.1 + 1.37.0 CFBundleSignature ???? CFBundleVersion - 72 + 73 LSRequiresIPhoneOS MGLMapboxMetricsEnabledSettingShownInApp diff --git a/mobile/ios/fastlane/report.xml b/mobile/ios/fastlane/report.xml index aadffa85f3..c40a2fb5fe 100644 --- a/mobile/ios/fastlane/report.xml +++ b/mobile/ios/fastlane/report.xml @@ -5,32 +5,32 @@ - + - + - + - + - + - + diff --git a/mobile/lib/modules/backup/providers/backup.provider.dart b/mobile/lib/modules/backup/providers/backup.provider.dart index 0b3a61cc4c..aabad64c1e 100644 --- a/mobile/lib/modules/backup/providers/backup.provider.dart +++ b/mobile/lib/modules/backup/providers/backup.provider.dart @@ -69,7 +69,6 @@ class BackupNotifier extends StateNotifier { final AuthenticationState _authState; final BackgroundService _backgroundService; final Ref ref; - var isGettingBackupInfo = false; /// /// UI INTERACTION @@ -375,20 +374,14 @@ class BackupNotifier extends StateNotifier { /// which albums are selected or excluded /// and then update the UI according to those information Future getBackupInfo() async { - if (!isGettingBackupInfo) { - isGettingBackupInfo = true; + var isEnabled = await _backgroundService.isBackgroundBackupEnabled(); - var isEnabled = await _backgroundService.isBackgroundBackupEnabled(); + state = state.copyWith(backgroundBackup: isEnabled); - state = state.copyWith(backgroundBackup: isEnabled); - - if (state.backupProgress != BackUpProgressEnum.inBackground) { - await _getBackupAlbumsInfo(); - await _updateServerInfo(); - await _updateBackupAssetCount(); - } - - isGettingBackupInfo = false; + if (state.backupProgress != BackUpProgressEnum.inBackground) { + await _getBackupAlbumsInfo(); + await _updateServerInfo(); + await _updateBackupAssetCount(); } } @@ -415,6 +408,7 @@ class BackupNotifier extends StateNotifier { /// Invoke backup process Future startBackupProcess() async { + debugPrint("Start backup process"); assert(state.backupProgress == BackUpProgressEnum.idle); state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress); @@ -431,7 +425,6 @@ class BackupNotifier extends StateNotifier { } Set assetsWillBeBackup = Set.from(state.allUniqueAssets); - // Remove item that has already been backed up for (var assetId in state.allAssetsInDatabase) { assetsWillBeBackup.removeWhere((e) => e.id == assetId);