From 0cc5ef662938e52e8e28f3d90379b2ea45cb19f4 Mon Sep 17 00:00:00 2001
From: Aditya Rajput FileFlow scans your files periodically and organizes them according to your rules. Features:
IFd7(K~mGC>JPyM3( z=Yd)yybl*9c~}%lgtgD77f7DwKWB^@(_JAXD@%|k$A*+rz^KlV@G*M0SREL+oi^Y( zw6AO}&bUrQ`jhxjpUHcqaqh$W*=w85#^gCUNL)pb_E3RPEB_tFz^bxhB7|V}*?1!0 z%{fai+iL2W?zw^apRY%r8xK)dyL%S-9{?rtz~~|ZF_C32H_oJb_cpNKsH%)Gv6_u^ zO+dJfsKNxk0gmcJ{K|3Myz| e8 &+<4JKbNy6CCR8NavxPu zdgGY7t$bM_4CJ@#g#`)uk}-EA8br&cIbQc%3BG?{%I+|3HC30>u_%@i4CZe*Xeaq) zY2)he1vJGz>Jq`oAK7o~UYbOm>*j5LYQ?h%@wvdI?n=Ji+~stJ;KTf``VatL6eMXt zO#4amxriT1bsv5$B-ekg8bjy&s(Uf-3Y8=hq~~?otLEegyc^C7gb5^A2M|+Hk}(O_ z)8xU-l9n#Vd>=$2q(uX&w8U!SyqlARcgE$m@4y4dUbo<2LE;}+6Qopho3DK<)jW6* zLS?A_JVa7av9cojZHQfP0B+*u*t_RJW=Ro1QrAD|4Pq;ji|!Y}9>W?P^*+{~5@qH= z`X`;&BTvrDt7D6cBkAoRP&my5Rk(m@jqP!@n(p~ Tr=WPX)f?OF#;Zm2U;HjE zSV};^!{bJQ>ew|jJ>C$!8-O2u`plweD1I%-k(iY99w9V=;a(jK?uCf7X1M~c2lFG8 zk78G7w8_EjinnGb{RrjUy5$fv>xs7g$dGhcbR6#uU(XIQs7-S1=7TsqtJ-JnhjDa% z<}8~+ZnM|UCwSY e?T$XtSiP z);Kpeix2Wy?<53mHQrqvaFjd<7ZpsT;(Mxo>&V7mDKzf(<5oN2JzXll_OuHE92uT4 zluuKQ3#12K@2t6vGRK4I*!H^;Rg#8cg~xakYU7Q7Q # zGE!yeZk6hqZlV(oSK!r-1#h)NtUEv;F3L;5gnp^JJ#6E8E&Byf7*$9?Rkm1=NaHTg zIj*T$_rBeXfCu@EM?!<{b^((w<$jJNRiq0+%nbel+VO+EIzVR!+gL9WJBtE?XowB! z<3&XlaJ%bvruH+e^qtYeu50<_r{rL&Q}aBA3 0p%W8u=_1SL=CLT@j5~aue0gk~DwYkT+ZU z@Z;uPEikJ7y|hBX8Z3$p2FvJFGxY4ZzyF}a4O8@MICT#t7jZ9sCs)7U1fgOuO zLJ4HH8@ZGfS^WZD#0bX;UpD#oqss%tKZvuO1_P)K7)`wS`F!vFrOMVd Nh!=%@(iuJ$D(8=J!9UfRaR$QW-|*!jou4R8wgLp zH=6{hpvd7&Ra1*rY_~-Uvyqt3aPtQ!)HGSVo)+L=3p3YZfj}ow#GtKoSO&YnE5`T) zdFGvS+tVm-Eu_8Y-|Tlp?QWyVd9Lp?`D;pyH*Om!wY;8}>pG!8q-B&FXdu|$p%JaS z8}{joA#I4vBYVD#W;4GO1zV{c>pr_h=$D?trjKIpVSQOFoSciF>NL>~gkrAQit;oJ z#Kqoo-;D4;)tjBOq;MXD=7rgK91VlrGeTap;as^TSC0mdqYN@|Q2J<4m87)PF8R~3 z?DeN%y#mU7iwR{FmEv6GmBCBm(V)>t=99y*aSryQ@#2&!%SIM_%II*xsH7X+iP;ma zv5A^AbQqA2a~wQA9#UXZF-^*ONW`d0ye#w1M)OlP$HN}0Q;!tAX3<2&VLPN~t9c*> z(gWEI+pWs6UZ~J$eW*q|PQc&v+t_TrF>n}6x$*YC3OQDlp9Ru9?*ML4A7!h-awpb7 z{Gi66zqjegxP(9!x^B_?$ymt&?oaj%DJ`w?aI4-QpDj$K^4L7%M+DSZP4`aU5eX9C z=#}Xw^=n&v()muaQvOh0mjeiv B%X4pX_(?JW8jGCRceD zMXM9;I
i-w+Sj|TFc>AhqIE0z2^9|(x}zaP^nM#iMfk{ z6$M3_gQ~l0Ue$zIpgv|s4cY0XH1~A#6Z24B0JNSXyhjvIR*9E?8ceByWiXK|M}gGq zM=&&$Feaz2p05nLKdoskJibU7 zU$hIZd{{0%5GYmILEkZ6jaf^58~-6@e8igWb+Yt0T)QyUq4m;@;}Q^kzB2+5({IQR z)?qfoc?Nd~wtn>*c$dIty5h}K`_}C}%xr`y+-Opn@#n>r@7!N}w3%b(t3DNfJ$7ws z-UOuq))x#7A_o8#WITF9^Ak4%sA={-YWI@0vKj4+YUxQWLxb_8+P!h88(&_SxqoO{ zc^d4s>04(6r~sn^NIy5R-eL6qu%mu9kuG80duqvRhE~VLLf82DhwoecNHn&>Jnk8D zq_8Aq9rng*>&5l>**yS~`FxcOM_^Uc<+$@xd8XTTTPI;1PR}VW4Rc+3COOa}z6{h- z*2Q#YgYBl+dvt) 9TK|_bp7bl4dwNu;)gRz4$g@#XedhaofpA*&YEu-I% zj9ti^@qS=Bm-(rzn3wAbpMLeY1O@sQK@36>WKCf?E7=Zvo~k!ysPsOWp8SQ%NfQ?> z=USd-emCO$bfPS(_$I-8n8##d3dC*5Qn}VtDWI47R)QKxUfafDe17OB=-g&hvtJC^ z^MLwN<1-2`7 r(dJ&0gtX##?v11&f(;r%Tt6)RC4&(=k3qh9*uQOH?7Li69a2ZE)=7=1< zdMZE5ZLx&e&(P^AHF^m=SfJ&sYRbyhP7fk>LBLS(sly_4_U(VcotdT^vRiD;HU3!4 zI8alFt{eN5u{)8bH)F||;eiBsWV~wQ#14cx;g%d1%Df`zhd5B*e_~9mY{|Alipl&Z zqweg{u)Q)_MbjP$s!TMUDK65qp{aF +$=_7g*oAKsoqr9 z5MxMYHT?sCJlryf)Y1=tf 7bLh4lB=MO$mR78iuyNX1)c1T>VzP@rX?| z>o>riOfj4BhctF&8N5{#0?BI+TR98$^t_HcQ{S>Uo>a@4xG<*I$hnR3tac|8jIh5g zPv)$oAExd_v*d+tYk%#|-M#%((ie;MXXvxoHju
}g+OSIMgj6~!cemOfm$^rBEHr>dQ&3bvmz zn!=gyC)vzeevT*%bH%CA#C`t$9X(F2xLApZh-aZl1is3-_L`$8N5?k?`TUnz+b^CA z*4BAPiP@0&rssMX5K}4ya18* vapc0kk(q5pzL?|_HJVC z; Ogg-IhiY(^-!P7oI_hJ|oN68EmY9 z)y4WOuD2eV*ow}WTAb@Y_d?T_=ePjkB9}S+8;rK7@`qeB(dYKy@P;Yw_62f$A5a=G zly7kG$HrF_H?SbZu0n0XxiGz`oSL^@4r?QAqCJ;H3lUsihG{cB7_Wpe0_6XG-7oVb z#e+JC=CT96OUM^2%4#W2eyeba(I>wd}`m>Rzv{xIs#gI ztcK(<41=cpU>@hv8Gmd90yK}4$Sg6)mI-FnJ0kf@w;&_9jnvk(#ino7LC&kQzT>_O z_~@(gXyB^~$VLqU{5HtQ5oSZPD}p;rNf^tTNu2%MU%?mgpF o}Yi*e1JUQ?|-Rg^ytcr3OSw@ z@ajv-PD^#C!3`iq@FRUBe&z#HXueL>`s}uf%(RMJh!2BnfTH@lGFNlHdN_=V848}2 zbT_t{
c$=wV z&F+uB4;o*gfqn_Qyg>pX#QoMcI;cxj-m^rl!NtJKHHMb3MjeoDHbDpVD11g4iR%_I zScl|2W4@9}^_zDlXF>8YE2YYPgD3JGb9UKB5CUs}`DIqw64R)E*#;E_VG<(X_tFE8 zZ-TCbqR0Wv>kGo82Ts14aY}4>`b;!-D|ieuZcnAz<-&H-Yi*>vPvSptu>*Mj7|hY^ zI{Gx`L*&ZoyYV&_H$!m8C_hc^3&|iLg&!_oaIDsrQ2`Yr{K^D|17`97=HaqVL}aHQ z1C;i$wLWWQf7T6s#DL>N%4>`@r(%Y%Twx@Zjja5!!h<{*_b4N+B!?kS44A++!*MDU z{ZyDH=Ns-=7$HCo+WZP2p#n0NCM6-Ef n`m zVs{*ab=^yd^&C#CD_m&?NO6h`P9H_zj=9hmYCRX43}{{HYZ?&gQV=r-*Ab$13XL7j z$|~o0=mws@
J2>U8FH;x*}{#w_jGxgl6P2y7jP+b!~b zAmNbA3 0edW z?Wc7zw&tlHjwrLrx`*=_3KM9Y8q7;w?UxO0>B}|-(39%y?TTvoR)NfTN4ze*0J6j# zZ^gl6-dcrYRzVQ<9P8AJ+Z;?#Q0k81zzeTX-l8cgn@-=^m(5aDNMQe%LqzA_ z>le`5|Iv`I;|v20lL#mj8A>cE z2c`Mmt$f$Zm@V3k_B1~)=O)^4*K-2i$03-^Eq|?4kc Vkc(K#2g-7xu+0|Mg&T`~
pA`cY5|-;n&TmZ4H86nRvT}4-CrMn$LLf%6mL_EI*N7 z@~XLk8-zXjOSV`X`45*^J&9xNc7D2Gwj_oxi_06KfEx0iG&wBQ*(oY2c6WELdRz=I zR9jYagaWO5v1c@+kDn9XyLn~`#>%V{;xeqs2=F+5{xgCEZIkY+lHv9l+4xIW754y= z4>X2Y>irvB%jN>94MIOqGnDhPf27Vd&O?ISjckN^H8ypm7H(~RIp%{`d)CwLB*~ n9&CsYM zim7CpB|=@S!a(Lair?6j;y;MjL2kR~8Ek2}tUUdd&`i!~zE#n={1YZ7mOh8kvrVdi zuRDpbdE$m_gm_m~d%dBa88vkcf!+PksKD|$()QJ?UhNHp(x&-pW@u |uu_s)XfzKc~(y{-y12Uu*jVA*~q9S!*weHKZXf&vh zf`+D`s)~(`ZR$)5Sg^9(=&V-nB`z+mshK>Xk>Pc}x9q(7%igl%J*(-EdZlRu0oLZ` zW>{DlzYV%D#_%Qy8sT{KT6p>QH4ZOqUi;J!!YiLluFR~`cYir&>Lt^9k6QH<*wtAy z@J6!laCyFV!d+f%zU!_J?viuavIqz$`f_E0Oi=R_{7m 2>PHAHO&Y518Qgs1(=};6P2SBVe#w-u+xF8Bed5& znqTG+&0c@%X8a=aCWlGf!;xCufpB=bzK+!1nILZYz2E?c6Hyo}dCyvU(6s79`Siuz z$>+!4#860Kms)gv{o*rF2V|v2DB{W=R=vhvz3almS~Ue32^99bZGQ$B>DaN>qN0J` zzW|4Y2Hr;jZl?(xwOC!#0TuYjr>ypljZh&SjFbs}yWrG@eqgsn>gZOi+=JQQZHAJK zV4JJ$&y0nM^G nAB@v1rkG56h;Zjp`EcNota(AN8A%rDXqpqAh!Cje zIiTg~&8Rh2)cNO(9#CqIQV>Dt$nnK;{4tBm>Dmu+2{)tdi=9zM-6r*IB=_q>d_I@o zo3$L^l@e2vjf&d~ctBW!zJNtw%H*>;9&`CJx39jIq{AuE4Xj6RP?FWNKhWwm99gCr z>K&oy`VqFUFYSrO&<#T<6Ewy|an&{UBRE9K^5VW40pZk^7aH(5izgzr=`%sJlLWLQ zo0U SddI!&gdQf#y;!5bdGF#6;1hhg>lH`3j0$B2v4`{0FD% zN{eTso8U=5b%u<*Jm=epYkw0H6UL@fzkxWYt(~2qpdilY)UQViu<-E3tHs?}{qQ*r z4LqEjwRd~M!orA%h`PVSND;aL6hK2x!^A?vv{74h;`Pd?k3X`)A=o-cJ=ACtnmO?H z)sh=LD3$)B4M%90f);8FA`}z?s>I^g-4*0tMkO71wGLnMc<#9+&JOJGP>6Zg EBTb^F~`V#X(5_Ij0%^&xYqyq~--MtY5qQo|d z$QaZ C{W?d;c{nO|D)sBf{cUN$9lb; JxM4)+%bgZ2Rr7h`&w`UlbPg1;c}kN< 9eCgmeZ*B-z>=3uQiRN`9Bh@+*G z+)*CBx(_|TlWH<;z=acohpAQXI) &D(JYPun(` zUKQB59#~MEwElcs9Ph$%mcYEkg(@PU>$XcA{i&M$))3Y{%I&9c1?ms-cZ&1!0Z>;? zEv?oY`nyx>_u>BF2N$EL@87>G=;@VJzX@0sCPDEdJ iRgu zrgn$QGvr<0BSuq3pt|h5N#vis)5T!gu|XloQ@vz-DlY4Mcn-qq^-g$P!-R$zE6(U( z$1kGwDB~~+M?ABeRrmk`pwxy`cNsr-$Ebc*)|s0QMx7v05pNsbD=2D*v#T9_lp#Sw zPzq{>XYf7GXh}bwBhSz?a&$CfM4dIkPC-%IptqP)+zwa{PvkR8cso-3Hld%-#A}iS zP{lEkIRfXd{}b!%w- B>L;rSuDNyB*jXlrW>j12roR+xz+v#xCbY6CL` zu(7E=xc!4#1puU3+9@Kka37c3f}e}BmcjWE8~BR+&qdm9w-$yC!Dn%~nqH5C(&e)t z_h0#&`^b>TCQLyH`VjBQ>m58-n2n7$&QdsHu?b(+o>-ZB9N|QQ;oSd1?3)+u5-RR1 zP4%yiB?U3<{%``#Mg_eSsw4o7C~Emro@7;ZiF^y;f
fHwcpg6y z=& E dO5j8eIHh=CA=lP9R) ~zQ^KX!lSMoIlY0w? ztM+*8Uz80EvnMq0@$rMuNv;F=y(G1@myT*z1+3 ?DV z-(X`K3))}qj`O=;yYw8dvI4=Utxf3R$K~ba#)cvIP)z&Bi;pHHD}lMWz}Dw= nMrv;Pxe K|XWjsQd~2;`;}JokIOekYyRargVlQIlID(8m~gf%!5$!TeQ|Z~)-~TLdxx{aG)G zpxfncaal`) jAEKRu(7qKUZ?6fe^)5`3AnoY(-zn}Xo?+k8A5h$u_;gY jlf@L|)sqwxh*b`81yCIBMP%%ldcU9ljkeHx7#1+uJvlW7@%rK!Mh2-E{O7^eHUg zWh9L$gr}2EJDeSh&){g1ft2;y;v-y+z;g}#tP5{O+I{78&e}E+!W4>u)C&h0P|N{& z{c@M)4p|qj9{C!ZBc6#E`9Ik;s+0OL1)6I)TN)7(V(0#?pr9b`gW8JYq-=uc&m6yE*%XeftSlSwGPX|R*SH8m z-mgQOjTmYi0AW!V+K$nY7|DA4OiMcMXW>8^Ixvz>=^Accet#_Mx_He)($>(O {SRz6!QRr+$_9Fmdc}( zDTk|%Nl?w!aWY>E&Y;5@|G{k3L&J{@&I$+!0L(qh`D7(>bzpE%tJTZX&F#KWDL*tc zRI}D5Eiusr=$m{pTfd?Ki~UyL&5Ao1F!L6s`%o%+`lE|cK>(NmulDt+*V-(P3O;Kq zDA-;f&fnhNq7m_g x(FPCQg;z=xenEY&krF9)D&?xVOkWV2{jwj!8(+7S^mgdig zH(^O0qWHJ;=c>Tj3SrwF)7e+rJt9bjDz1WTi*HWe(~(iF`c<{J9u=S8>UBlm*xlzY z !`fc)GUD}%DdIB0I=wU0yTBy3x59zJ9=TP%#>A+@8U SG}xPuU^q }{)gU?ETKRe6&h-sah;o+YYOcn__&UajLYCi*b0n=;N4ctPg8C< zkcNhaEPoPdDzG{cQ2%Df;EfX~AGx3qrFWqNRp-$MuKMUL+E7LaA3HHHeZSiV1hnb< zWcr)?_6IbC6}0&5BNr5kdlHGFrGcn?+H=4t{=!jOW;tqtDxuy7X@S85NOXGP4wNmD zL2HB&5s7?QJl|S%xZt^HKa`u0JGw `ceo56w9RXT&24o|3JmT#sWMM`QwNuK{|LYk4C^=7Wzr~=fcSZXBy ~OLUmScR+(e4#5R{fQXgbvUz(^Dl|TgJ^0 zXWWwBC=zfFg`X`F@U3D59{RWYwCz?sI^I>+Pb!X&jG;6PT@e1ajp^E=i(g$fzzC?7 znj1AYj88W^B&Qisw)NPuQSVgnlO4lSt{&)3>0DQ%uuV}gG$ki3KRWywPN624Z*V&B zetyss3v0TZkdBLsyCw(ns2uT#&z4In{UsWe#^m+&^}_}GY3BnU%BE&!2H?zw632(I zCEm-i#t8+X5fxdIExzaB;d%eQf3KF%(ue1ogfw{YBRA0C-B Kdl8IBI(Ed-ZC*=NO(y0>$M~nM=0gW2+^B3`E&3$iEy159A1- zf{o({d0k!N_an>tyJK0AA0q) 2N0 z&eWAp)vkJdKnCF%VNuDL(}I{%hJm5E{wC_gqXVFkN#*a5CvRun^X&vZf&1N@nxMkM z!ohfYht%zYxAupELKq3(UYrlpPl~dcAj!As>p7TsuV~76^wfglq??VCEloj{$30F( z&E5brl1+F5L56irFPH6K!Q*&sy(qlbD>|Se`3&bK=um?{Mi#pAI+@k53t>KZ@%TKF zh}U@M((~au@29@h3u$7No0-|xY;=Bk>3ID8YP+t5ACP-o1Xt)+io?x-g@uK(miqxn zqwF@+pcDwm?*UZCW`r)x1+a}BDtK6lGu0ZYWC+35{y&V~s13zxkN!%jvg6O=)9E?A zl$Jcsk-B7j7s{m-z^T{sjrOK8XjvSC$q*&8>4XvYhbMvt{L&ENDRHW6s}c10c?bYA z4j+Lv2pl)6#YIItb{hdt!R`9r^Kh|-v9PhX=K2SZkiSPqM*~2%4=$x_d=Jlp>(?vV zYT`=%p^=dl01eDr)kLtx!P6mw e=N-Zb)H=JOjSar&0}bt9 zdV+WNza=BBoqI4azftQ2Fsoa)lk9ynJL;7HyVOhmP=Wzw`;KnN`*!7Kp{amOTp!_7 z&@9D&94xtEpZoJC&mX&@!hz4q$`k|^wD_^MQlshoM2$*2fBrNyXVOu?tR?E}%bS}1 zwCb{Wmub)$I6oV~?{RBuZG8kF5-W1R66|D1XAZ6a2v|#!^;#&veJq2s!-?yn^#iDf z$ri7{QSFbLrjBvoB{-AwcHt`4FsgLf?l}YF^}cVV_sE$N&rHeKE=F38EFTa1?7>fN z$jzIPga;mTJJ_B~PeJI86T6~gF4yziMdSe_D5&q;9- B;NQl4Pg=2N^%1rDkT&}mjUX8`itl0y*-lyZO6Z^=8w} 0B|9fp{K2_t*mUkiauvSsi1+b|6clbcRc4Be+V$23~xyN( p;S|2yibVJ&L$dT@b-teC`E+Fv{L%h!TXnReN^+=;nyjHrrqM2qy4^`hdWZE4j~ z?zumMIP?)Z$f36PX0wCyNZPEfE79YKBKy4|qvb}unyTGB@p7V~0agLbHHO_t=r)*y zge6`-ig*0ifwhwbMMd`R!o9rspy3;sk&%%abkIUAm;@V}!u|Qk!9$iR23Wz^ ;Jk3pX0avqM6^y(3TB;Z}>o&Iq7+<@`g#!*M*3{rAD#e-Z|#M60W-DWJ%f z{9nvkG)&OR0vfMV=i=8sW}eyT73y3gvR_%3;KXo9yN_#BR(^w@U=2g-&d5eyzHPax zzn>t7^bl2e^QUq-T_k-5-L6o|vAxIp+l&XjG3i?Z7U3FBbF0g;)n>2A$&BC$9Q|vq zI5~EbIDi4|7dNga6A41A_BQ9e6|~=yl$DLJ=6m-p3!oOSM`O3KcDA+(TrIk$QC ?ov+j1(K||VL}4Y9oZ-5nbyhVyve>8E6b6GhT*0>JW=xY1^GLg z0gS*k`VkDA#*Ll^@o%~`A1M%~jmS%=RHs7)1$kGABcZhiybm?|)NmJ5&vo~~!?ln& z!{>oGP!7G|_#7FSYsvLC(vkg;yZKCzUqA15x1?e9JTC9xeYBpP@+qck+CPWfoFgTT z!`3M!e)^l5Ln= BHB0MBi@`i_g8eR_se z1_Q`+UOj~5KEGQEd>Jtr(^hkKvi{ZH6^}xnpJa469@T)wx70y>uaP_sn|IcbP J!xI+WL2OerdjN|hcys$mYicG?b4FTqxq*v?Ce>w7GsB?-4Mp~o}>5UCP`Uz zxRzj-MN3&Sh!nu`cE6jNnv&j|R&rBQ_X>PdQ>7nhQNiEwxH(>~31Z1;(QEapDT|4Y zHXaYonF%J<{+sIz{oxT}?XE`^7CKPBZ0$*_NBNtld_4U$Oc;Z>No~-Dr{dXCVw*VR za-x#Tc+qqw}AXrb; zQ+1C?OS2{c%~{^K10b$Q<-=kpK>REQVX y{;)hXkSElezzV>I@G@c?crn{c1H`6gL$nbWJ-j zFfhf&MP6$E2svR|=S-BTPWIO48Y?E*ScI|Zx0vyU(u EMutQ#9*YbJRfPmG IO0NkD4=gojx(E z-*cK{4j`cQw9^^-&jeyXMj%fQM1!IY%dp`JFV2#1^5^s`5CBjsFoAzmRB`?Y=ijc1 zs5}FsjswDe^Cm7fc8(ayPk#_5p7ON<98(q?zznJ}CoyQ!p)P!>b%HisHF1*Hk+A0v z8(F(mJQ !J^NB+Yf`asg%mc<08XMdc+ zIfpY;cb1(?(mqf2*z_r4y~W=*MYj57nwdcYNONCe@@d0C>0}okw9mZN*TabEVl~Da zq?(_(xm?%4g7pMF241_9mF5aeEs#FrK~SZfF3ac&4pZM3A7X&LsW8P5RRT!9x==Bz z0&oF#br`@;=rm}>QI{IMaY!lO;s@hhHAdix0Lw9+K9FTk#XrOrZ9i5_YnD%uue8#T z(Ghxkq22I@e EN}f;|j&(saGt43ijfWcqF49B6G#cPqnh?laPXgRbj(zy_6B zfMC1l)!>)A^X&@EoN+F1h{x*Tir?wHe3vTcWcsHVs`~P&t^ODjazOkk9QBTI?=Lnf zq2!+-Pn{hdR{lVKJUBQQBOB{_UE`AlgoEo%ATO}*M)@b9f@V zP%0(QK5sZ z6RO1BevOzNf(8W>=1}Vo$n2u}NR10hh`_`*MTtD{Q9Xo&bY`TBKevw^c<$UtOM0Ij z>;tT;(+v(ZBYi&fB0WJb(i2ab8ZGD$;NRlw5cMICcV}m3D|4mW8*FwpckvC! oLHf=2S9}$)Y3Un-q9y?Ug^G%Lv`}32XL;Rg!KFms$R(@x!7Lyy{Ws#PrU1X@e$0d*4 zA9eISl@9Lv+>LfY@|uWg4q5Hf=U<($)YbJ*xNti2u1FwFj>xzLq62QX(3_PQX%v8K zd1{9zaj|H`835J+5Wz3jf=?ov3Cm16Iy*C5|KN*-(P`Hya2Rr=ta?9t;^q&^?dDOn zT+6nseTSke_kQ*Y+n%s4)GZ@nGyhKiO(+{6wX(B2#H0~AFkg6jWnC`VKQUy gL0pv52O+L1x3xdukFs)_@mdS6IBQ^{x@a$r>N9H~s^yy< z)W#+#QOqa-{}CO2g+me{8xGy)pkD3g9ypl{V7{7PAygSP{2eLAW`5g}J<`7I2zOV} z7uw&;74vm=IaawU#i{^9*pyJ?z7ay5OM7z7ciu<-f pl{H@{TPc*yGU_x;#jWi#dfyEKyaPQ!NolFq z`Zrwd+IOt1lLBjqC@3;=FI$awWR8o>N7eOwAlg>E%~XaCRzQ4#Moc|lETyXkK)Yji z=cDBkMOpRE)S<}h$=XV1_vO~7mx36?h1Z9A_b2kJM)QWR;M{unlya&Y1gmUT=$hMv zJDHt02#fL_7`3YshjFWqTqO#Oft<90g${&6r+#|ecp@$#!JGD)h{*H#>8ADRdeO$} zW;032uy A|F43C@3hje;{Q)Kb?42 z{HUv|17?Y3Qn|_t--`sIxIP@!_C^vFJC>>Gc{=r?-93TT)#refE`ZiP{bXUgzdlMy zPlo_@#}c%fGAi&;GT8!_HQ-1wDMSFGc5SPFgYEi9rI4nIR~rJJ_kfS)nMT75IQW}P zsk^9mbUN>p|Ge*VLsje=W@R$3V|hY1x}8NXXjyyX4y=EQU!0b*q}WMh_Na%3zjhmu z-Kn#HE5-lv^#1PC3jNO99V for example patterns and templates.]]>>FC r!a3vW zC!M;Ms0_-p_9d-04r&Bauyz`-8}xGMm%goT!$w9)ieCCnqPsWQPX^Xg_?w5@V*L zqhn@%`R`IFDaFcW9Y)-5j*l89Is-`&EN4nAq>K$&k^ts8CN53|m_pOjBf-a?FH$K6 za33IDfSg{~i*>#DLl3NfD8z@Hwwoj6jJsU!Dr#^`hoq*artvzb^EegN)XbQ(0psR; zFhdV8Dga>*QZ3W9UR9qJ1K6Y*lR=#P`c~6QSAbdpCRfZx(|?tegacW4R1^ubp{SDV zlIkx_cC!(}Jh&3g>Mt`T8Z|*lNlDkQGl3N|yj1woORhU<(g_UJ{8r}XT%Pw;%}C|| z8521zhuGot2$5%AmpEMrqr5_WBP{$wKn}J>9b@~kt(048)V2FzhfqXfqo*)o$+>nG zN ?ypxu|a@+ 4$=uAt_!rc5+ccwyhYI>UgYDx* >9UQ1-lVj7
l%}L+@MYI7yk9 z#vsEMR#uM5d_owdA}>?N*>mcj &BQ#gfJhjj|c=jZUrG{= oV+cAio?UM88Xu)KUcqCQe5Tj$Y6RW;s-NQ;14F4cCF*zY$08nO`L_3C7?S|#^< zNsYL!ihI8%yb?>@EH@ix9A?da&SrHndiQ^{^OX-#bziuLM!LIO>Fx%lLAo3e5JcLc zYbX&x1VMU8K?Diu4pBsE5D*ZQj-iJhso@@e?;mkL+&drUd^!7^9c!<(&$FKOh(O{t zIg?eK2}wZ61dDB(W!I+4CpYS4K^#31EG*ls9QG6oO%q29LVPJK@<-sZnMr*zD>wT3 zl03-g!9$wD2(Zq2bpSQrF_XLn8X39LLGRL+*gQS*F6`jFrkM>yzMYLt#na5mFj}i0 zBO@#lE ~;{eFH_uhZFYW_RIBC^I%T z)>J>HtiIIfrb4}bh$5;^_KVZm)%EL_OYo#}BcHXuGqR83vIIm#3grA&ZgFHi B?~k}qDoFzT5C;p%YyTG!5Sn?y17RxDOO%0$P9l=+xoWUV7yd{H}v^fdTj zOlP;MC{qa;jFN^6pY*D?{&<_3kQ2luq36|R4A2or&L*OI4o~hVYoa1s*8`iXyIqXh z)=r2?907vJu=9J8>;QQDHVP9s{?g#<-DYit`Mx4oP0HEX*E9nO1G8-9Wo2M`SzAks zFvi5r&hF8p2r#7#t~P460f{VxowW{$%g6-o{;s 84i08!W|o$>&j3#VVAi$nF8iEv zOiT=)LAID xQ@?)MgUO7dq9xF%z=Kkd5W5zI6$1zsCpvyF`Z%z! zI>2!J1aOpiypOZFpdY<>HxW$Kdd_ZQ`LueFk8~5`$5vWePM9W>{#&-&{aa~0?r)<- zTmVT(HR%=hN*fiG%`<_-cT&p*U?USBIuJB4hT_D;QqIslYUk#ptS%9?IP&umMfmZt zvv=a>FQ7{G;C6b$4fLX@_f4E2dlPqA_#+WTF(G8pB&7JvAoyvF=)6om8FDxyzgsf? zF?U@+`dl>u$(MII&-(AlT#rnU`)x?&3d2^&xCVaI5t3oO3tO9Rd8fH1*Gj+Bqvb`4 zja@uB>mIpnVwYSGm1|^ACIx9Jzif2scU)KR$2P~2ZT?}OeX!)F!AwT@gx0^y=$2zX z7(7SKS>w+@&DBRW*H3!1P8!9*!9me=p^M+DAjkm_oRuuh+rFVA=37i0?`e8%&K 8#mDa(H$ct>0>?SAlwhWp?W~JoF3#WM_oeLwZP;duaYCac`BBZ8UTbfbJtOG zl~gTSG)f}@%|B j#gsI4hT4L%UHwFWtsii|(o2l9@Azc(JI5mh4A{Ac#^;=S8o? zE3-uAi$oB?$E|A%)I^}2NEBH2vtMFqRt-#5@}O9>-41Gdino=PUQEF0h%d_8xQ**A zB(AR{uY&Hr8%hy{d>cSHeYM>YH1*fzGJKVIe?TNMlW0V)*LdA$1@naNf8_>L;L*42 zpaFR{BWr&CTeDIwr7s*0!R1VWd!nmdOY`I{j1DZHBj&L^V(yCD{T(N5m^ KYE%Z7gouZhF;_|nB@&zz&SDH=DDjb2QmkmPJlG 0tkp9Q@9gOZT( ZmUUaxv1iCha@ 9?_y4hIr@&HN< zt^OjN0U*RS#V-mIy$U56Aba0RL0`#{NJygP$$YYvvT#k|ay3s=_PhPc^4+Rx(nHH_ z;(#y%p%P~rPoBY&%6p%k8!DUeB}%u|n5RpivT@qh5A5N;yKyu$l&70*XyFbYMTCS^ zHI$?7+|uTmL3;`k%i9^t+(-S2Y!pSMGJLIYcxXlN{_w~+P+|V5R-yTcE*>zBorD(; z?6i2cvr7BE=?fRSl-=I0ihy<{aun&Otqc==KUmkxHeh>WtVKyL>a>#r(u-F-mwwC0 zMd*dilOmOnYcpK39-ajMvtq{fExy08g+q(DeE)uXP|Hu(eD*L|mw401rtWr%Im{D+ zSX3KFLDyVY-^-dflnYsX@fvaE32ZZPisdFfm5b;*-q5v(Aam_4G5I;hFxQNZl>!iq zlk7JE!mKy9Y%(h)&~@(jX8RtUkTZFXfK;+4NPw2{NAm>c?B;LFrmSSMcV9g$w&Ej8 zrN2Y^o$+c~*Td(#3uOEGOL6}dBH@ijFvbd$7}{A2G0EaC0SrTeZ$KS}TTAWNgl!WB zE!l(fXdj7b-K>x@opB_=ineYbp=)r77nKc)@u%msJy<95oLic#{;%xFm*A_s)*FZB z5dGY54B1h!j1>NQ-tlrJC0!F7Hh19Ay3hZza+7%d|0}0Cyv1)`3D}Y$t6UD;^&NE- zF0nlq!?%hpb?h9f`s-8bV6xdu9`u=a2uVy2!fy$3^`^CqxMJ~XPQEp3l3ys;mIq(E zpdEYPbcv>;jc?s2>C 4hk0> zekSE%*8LW%P&odd-C4D0oixp-%5Q>W?~gj?gs@x0an;H{kEv-Y`$I5=1<-86p%1Nx z8cIZN6e7Ri?ov}v9TF36d5oPPL>l+UH>vEg)%TuOfr|w%Ds8Fh)h2q{%iMc)v{&+> zgtc8C58IkJ1iC}Saymtt{kVajTVR2apUD32%lbtr9&Ye4gs59~h6?;xbGFbgLB$el zxuw29dMfzl#>CEfYAyH=(Vq*|h?FtaNf~$A)ZK>=vMB=9d;Kv2sGHr~Kk%f0Q)J3U z)d$@FF*xttf+e)lP5sjJW7t2n6u-z1A;19g&~Jf049k(kp}Nm_F)TvSibB|u^4$(D zuE59fEw &g2%bOG^s--I+zO-Ld zw4b21_~TNV%0R>K8v07nr9G?9u#_{m8!Eo^CYH(_T*k*|AmL0ssz~eSm^{-CZ4ZU< z7kQl2kTo;)x8t{K@n4nQ3jy0Rw(#=qx dK*r+Y@uZG7_UimAM!Rzwx z@+XDXXDHL}e7OIlR!Z*M&a=xRj((JJS0EP9T6Wa8waVX3*M!nG0L_L%B?cV4F#Nc$ zk2PxP(f|kpjpji3HmIiQuLMie&5~glzviz0J1`_vsNHd+;nY@16vKc2hyZ1c11xdP z1(~w|<`&O*uvWOTQv*&Uh0p$1f;M_vvO?_Kw;$_+ZGQ~)`)(3QhU$tJwJfSSnXrf| z5$nZZfAPHkT{QSq+r{ E2M2dnxd}*Y{*b z+TeIh S3kB*wAXUQmq3>U!ARKNH1AP50x#d~ z&BC3Zb;&wXX#bj8(Grn?rL1r v7I-k=sf}s(w53Bq}or- zDg1OZvuHGHqp=H =TW`Ka0 zDTQx0cyxF-+AajwY;;_c;|VlvmcWs@iOPMJVza`<0#`?mk_Va3zz ty^|n(vA8)g-0~1mkljVy(YP#MO>>mU}7h=*r;w!40IW{ZqMofF^OtV!#i* z$d2s!N06I~iEwLYE sWuE(dpD&rA zLN{I7E1u8NV^ oDVU!BY!@(>-QfbrJG1rjKs|rKO(^HAeUU zRWCB0uW?YVG}&kdoW 4Ngq74+A#wGq5IuPLu_fJDP?+-Cy* zuAx3Ns}ah;IrGCdHTBdQJiP~35q>b0#oZe-jp%1VWvHFPHjbZH!zXpj64Q5<(%Vmu zb20Q2YKgEB8r;w@{WI7RU&H>7@%g5)wLpJb0FYP5jxwk_dlM_&BzWXux{-7D+fP+q z^OAg&9!_N&YM72rbUa27Ux{~1%E0QTtfn*6ad^{VBzg`D#SDLM-he*K9jJmqgaDvF z4i{i_5wu!NKk=jXv&hV@s8COSutty;c=~cHXrUS}t8mgILua$ycyq&UjPI4iWHHY6 zE0^<8CyAXcdCd0@s6!DvfQFw6a4}V4lmGh}@&QhcBg}?y-xI{~ex%dC q?AW#GTk+!S}r6F2BH^=(AN zy6>ZU6DacT^e12II!I1d{&SJ%m9`6bOg3fublYd5TSqA$J6CqcbanWe!YkSv6fvr& ztX`ozi{va2!)tP&=&3k94daIWXAJsA0oiA#p&qg 9t+i!{a(r1Tey4#&+RxP6%D#7x4nouA$1hF-$s N$ I$Uvr3};pH=e}C)k`Qhx@C8-Xo}O|0rnh`|_SsWxOk80H03AR~U_^+H zSoQLc;z+08S82Nu|CLs@>#e~p v<1jDI%VSP{f4lR5dF@Jix#-^7%1pN1&}aF z!anZ@jSehky(&rC)uG*F_xedqUd}%FlBEbns?TofBXwzo=G*&y?P7K;)8Nu9N)D)D zmLnPP-$K6>b9;#9p))uH-ljWmn>VxgSh&61Kp5YMzZAy09(bvIPR#mobaXw;dD#up zE=-3Sx>(1$m4X8p%%`cn1Vw8Ve1@h(0=9+f%ZiPb$ws^7Sg(;h1UlL`3m#izO+hnl zMidHpcsgki$Z^nVa^5VM3g>u%3&f62$$ICiqKVg^SgZjy=|oM;J5wg*ZO0a-X^~>X zBQ#qRV>f1}FQF9I<*ql4jeGW8`+sO|0N`f|*cfrCWj5T5cf6mGO_-}(!5;9(udC@H zJLaRF9s;ww5WTI+-`ZrOijh4%`LWJ%EhgC?ak9)W@@k-?A>0d8nPI#G)E9s1@U!mE z9!wW;qvptir>Cdsdf9(EOqX)6cZtoz|9y9?=W%sGsHaKI^Y2NAKAsnlN2~bvVc3B0 zJq>Y}mx6-TS1-fJNk1_Rh^QH5Zo~MR+ zY!2N5ZlDSsheI7sl+a;jm4~&e4+0rm^iBS4xN8pdnfvs#@V&d^-=1Z>D+s^(TSgF( z+m~*={+U)K)0JHRo%3a5D-sihZR zZO^O7_N;7XIcm=hvO@}g2R)ri%<_J61cu#mZQS^(=OPS>p=UGQjgnn=)W}lJD&)HB zO{XTMf(l#_6QzTu+lwzIyCwM2J7>o4?=5~(M(tRqmvn6{tSC9UN*t4`C{^&!-9Nm2 zS_b`%`}>b_^!7?S2s>|kKIBvUAH+r*>*++;79^7i_rOT{)}pe?T(?4llQ%taV2b*o zTSwpHfS0t%LwOp}_Nl6mn{#ZGC_}=}OE#v_*;hC~Z=uHS3%IYo_rLWrdf4pUB36#t zlO85l3H`+*<1+Va{*ZOIM_oBN=K*&6izjE~NBnoO01k4>Fiw7Zia5*}F)Eo|@d1$z zmhYPp9^N2S*m7GIs_2j&h$7D%c4rlE+K5;gFRvj9VwdJkzHE{s;FC;hVOhLh5p>|W zAOM!exUkd^->8+xYTNav9lWtg;5j3CCnNeTJP|%MRyd%X>k=plm-?b(yQ7`iZPb1u zNCN0rW1x^66gvNeq+|D(MeM-<0D*f4cs~3_=BBvO|99i|r#0;707+cVJA< nyymCbgM(ESAv@G#_Vg(a z62M{nAk2+=i>%_GJ3FW!&b3=?`o;LzJc DP+ zb9CgL&WY#3#aC(On^__C8PJ{NXk$ldJWhK?UbFkE`H|e}{Xy`7iJfS^bEMULlh=S| z8w8X<)1*G6CIX2~9}K%UpoJJi_pO887i&+6*_aGA(lR%bn}x%?{f-aOqo6rZ-QKPU z9^PH2X9HZuF!YGL=c5P9Q_m|73R#MZ%7(+qW_s#&+xqwN2_{JwL8V!WRG^5$rWE d=XhXLehU qF7McF*Z-Q3~- z#>;EpoHUx8fjchnGftF-TUXtiJw RZh3h7*D zbUUy0@p5-GK{d{_B-i=K$pM%pL?P{K-;rhIii2s`K(U2KPvF51Cdv1=w&%&E(Py#} zZ)Ifv{3|#8NbBHRoVDU#pV^NU&xFEGlYP) w z!D+=c5vAlP;dYeyTI)|i !pQ|NL_hbN{S1C2i$GTQr5ye+7omBM!HA3dxKcX+Wtmz-wnPRQ zW2e%2&<)yC8D?|B4l7&)q18SroAa8r%C2EOp8$MIDcjgyJt*)=!2MJGEi}zAxAh>M z{buW%Usk-v69gU!$+cSM#goj}17UBZyu$ #xrs)GfL z;t+2v3Wglh;c`ks8wubpd~Pbs_CGGyGOJbRI!|97YQ>F%qo<*ZVFra&5Ef*4V$km^ z+E(+buD|dqo@JVcntWrYOE(Qd=a+tP_VEe5BnTkgdEdqAS`fny>be?@f0yblT!EV{ z>6IHl@;}_|*g&*5m4r9)q{pmmQ;6v<*lp*L1 5U#-ErcOstUEuIc_A~a z$#)f7CoWyK`BQgjLDAL`2BtdVR8qtX9zj87`G5%vE0L?|s$(Cc{t-T?h{-Z5ziXOg z&Uq=Zh6FOgDQGXTrTNB6-_B5JL0EnT6v~Y+&`7e5`m4{*Yxz*c^>6m9 7Q!hme+4}sp*Xv6k*>Eo=L)<6-K2GkCH{_433z(!OLKilr E2e7m0BZPh^g4Q@LnJXjTkD{11|9pIgH|c*ib2-> z|G@B|rzbcX#y?nw>L~9)+|^|Nim|!cSQF}EkViTl?ms=z;+dQKTfeF^`Tgr@ZA%b~ zlw_G;R`v?G^ZYom01444s-F&OD0{KTe>=C#RAnNDq`@vT*vMO`FVWyer(+N5>iR&Q zBMcCOgLi*c1noVkAjL%)B8K5PgSwfanY~~}vkRSuRvD*MnFu}^{Ytu%BIW7nu^2Yg zr;#Q;2tqhc_pt$&Sg6tSiuW%xi;vPwLIlj&h%z#=l9?d#kJl28K_~Z-MdtCp!3L zori|*zss=5$TY_ RGQ2%imhxS9Q)&=v>?Y!>q2rcoNR~~( Date: Sat, 7 Feb 2026 15:28:26 +0530 Subject: [PATCH 2/6] Skip `MOVE` if identical contents --- README.md | 2 - .../fileflow/services/FlowExecutor.kt | 82 +++++++++++++------ .../co/adityarajput/fileflow/utils/Files.kt | 7 ++ .../viewmodels/UpsertRuleViewModel.kt | 2 +- 4 files changed, 65 insertions(+), 28 deletions(-) create mode 100644 app/src/main/java/co/adityarajput/fileflow/utils/Files.kt diff --git a/README.md b/README.md index 36ace7a..5372d12 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,6 @@ FileFlow scans your files periodically and organizes them according to your rule - **Rules** - Use [regex](https://github.com/BURG3R5/FileFlow/wiki/Examples) to precisely target files and template strings to rename them 🎯 -- **Actions** - Choose what to do with the files ⚙ - 1. Copy or move 📁 - **History** - Recent executions are stored (locally) ⏳ - **Free, open-source & private** - No ads, subscriptions, or in-app purchases 🆓 diff --git a/app/src/main/java/co/adityarajput/fileflow/services/FlowExecutor.kt b/app/src/main/java/co/adityarajput/fileflow/services/FlowExecutor.kt index 7c5cdda..70adf7f 100644 --- a/app/src/main/java/co/adityarajput/fileflow/services/FlowExecutor.kt +++ b/app/src/main/java/co/adityarajput/fileflow/services/FlowExecutor.kt @@ -1,13 +1,12 @@ package co.adityarajput.fileflow.services import android.content.Context -import androidx.core.net.toUri -import androidx.documentfile.provider.DocumentFile import co.adityarajput.fileflow.data.AppContainer import co.adityarajput.fileflow.data.models.Action import co.adityarajput.fileflow.data.models.Execution import co.adityarajput.fileflow.data.models.Rule import co.adityarajput.fileflow.utils.Logger +import co.adityarajput.fileflow.utils.pathToFile import kotlinx.coroutines.flow.first class FlowExecutor(private val context: Context) { @@ -22,43 +21,78 @@ class FlowExecutor(private val context: Context) { if (!rule.enabled || rule.action !is Action.MOVE) continue val regex = Regex(rule.action.srcFileNamePattern) - val destDir = context.pathToFile(rule.action.dest) ?: continue + val destDir = context.pathToFile(rule.action.dest) + + if (destDir == null) { + Logger.e("FlowExecutor", "${rule.action.dest} is invalid") + continue + } for (srcFile in context.pathToFile(rule.action.src)?.listFiles() ?: arrayOf()) { if (!srcFile.isFile || srcFile.name == null || !regex.matches(srcFile.name!!)) continue - val destFileName = regex.replace( - srcFile.name!!, - rule.action.destFileNameTemplate, - ) - val destFiles = destDir.listFiles().filter { it.isFile } - var destFile = destFiles.firstOrNull { it.name == destFileName } - - if (destFile != null) { - if (!rule.action.overwriteExisting) { - Logger.e("FlowExecutor", "$destFileName already exists") + resolver.openInputStream(srcFile.uri).use { src -> + if (src == null) { + Logger.e("FlowExecutor", "Failed to open ${srcFile.name}") continue } - Logger.i("FlowExecutor", "Deleting existing $destFileName") - destFile.delete() - } + val destFileName = regex.replace( + srcFile.name!!, + rule.action.destFileNameTemplate, + ) + var destFile = destDir.listFiles() + .filter { it.isFile } + .firstOrNull { it.name == destFileName } - destFile = destDir.createFile( - srcFile.type ?: "application/octet-stream", - destFileName, - ) ?: continue + if (destFile != null) { + if (!rule.action.overwriteExisting) { + Logger.e("FlowExecutor", "$destFileName already exists") + continue + } + + resolver.openInputStream(destFile.uri).use { dest -> + if (dest == null) { + Logger.e("FlowExecutor", "Failed to open existing destination file") + continue + } + + if (src.readBytes().contentEquals(dest.readBytes())) { + Logger.i( + "FlowExecutor", + "Source and destination files are identical", + ) + continue + } + } + + Logger.i("FlowExecutor", "Deleting existing $destFileName") + destFile.delete() + } + + destFile = destDir.createFile( + srcFile.type ?: "application/octet-stream", + destFileName, + ) + + if (destFile == null) { + Logger.e("FlowExecutor", "Failed to create $destFileName") + continue + } - resolver.openInputStream(srcFile.uri).use { src -> resolver.openOutputStream(destFile.uri).use { dest -> - if (src == null || dest == null) continue + if (dest == null) { + Logger.e("FlowExecutor", "Failed to open $destFileName") + continue + } + Logger.i("FlowExecutor", "Copying ${srcFile.name} to ${destFile.name}") src.copyTo(dest) - Logger.i("FlowExecutor", "Copied ${srcFile.name} to ${destFile.name}") repository.registerExecution( rule, Execution(srcFile.name!!, rule.action.verb), ) + if (!rule.action.keepOriginal) { Logger.i("FlowExecutor", "Deleting original ${srcFile.name}") srcFile.delete() @@ -69,5 +103,3 @@ class FlowExecutor(private val context: Context) { } } } - -fun Context.pathToFile(path: String): DocumentFile? = DocumentFile.fromTreeUri(this, path.toUri()) diff --git a/app/src/main/java/co/adityarajput/fileflow/utils/Files.kt b/app/src/main/java/co/adityarajput/fileflow/utils/Files.kt new file mode 100644 index 0000000..9955409 --- /dev/null +++ b/app/src/main/java/co/adityarajput/fileflow/utils/Files.kt @@ -0,0 +1,7 @@ +package co.adityarajput.fileflow.utils + +import android.content.Context +import androidx.core.net.toUri +import androidx.documentfile.provider.DocumentFile + +fun Context.pathToFile(path: String) = DocumentFile.fromTreeUri(this, path.toUri()) diff --git a/app/src/main/java/co/adityarajput/fileflow/viewmodels/UpsertRuleViewModel.kt b/app/src/main/java/co/adityarajput/fileflow/viewmodels/UpsertRuleViewModel.kt index c079925..313ab0c 100644 --- a/app/src/main/java/co/adityarajput/fileflow/viewmodels/UpsertRuleViewModel.kt +++ b/app/src/main/java/co/adityarajput/fileflow/viewmodels/UpsertRuleViewModel.kt @@ -8,8 +8,8 @@ import androidx.lifecycle.ViewModel import co.adityarajput.fileflow.data.Repository import co.adityarajput.fileflow.data.models.Action import co.adityarajput.fileflow.data.models.Rule -import co.adityarajput.fileflow.services.pathToFile import co.adityarajput.fileflow.utils.Logger +import co.adityarajput.fileflow.utils.pathToFile class UpsertRuleViewModel( rule: Rule?, From 79dafd67a5225c8fbbffd024a84324b1fc0e1328 Mon Sep 17 00:00:00 2001 From: Aditya Rajput Date: Sat, 7 Feb 2026 21:19:25 +0530 Subject: [PATCH 3/6] Provide `superlative` to use in case of multiple matches --- .../2.json | 85 ++++++++++++++ .../adityarajput/fileflow/data/Converters.kt | 9 +- .../fileflow/data/FileFlowDatabase.kt | 7 +- .../fileflow/data/models/Action.kt | 24 +--- .../fileflow/services/FlowExecutor.kt | 105 ++++++++---------- .../co/adityarajput/fileflow/utils/Files.kt | 8 ++ .../viewmodels/UpsertRuleViewModel.kt | 10 +- .../views/screens/UpsertRuleScreen.kt | 34 +++++- app/src/main/res/values/strings.xml | 5 + 9 files changed, 194 insertions(+), 93 deletions(-) create mode 100644 app/schemas/co.adityarajput.fileflow.data.FileFlowDatabase/2.json diff --git a/app/schemas/co.adityarajput.fileflow.data.FileFlowDatabase/2.json b/app/schemas/co.adityarajput.fileflow.data.FileFlowDatabase/2.json new file mode 100644 index 0000000..d403474 --- /dev/null +++ b/app/schemas/co.adityarajput.fileflow.data.FileFlowDatabase/2.json @@ -0,0 +1,85 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "b5ed230e3566b0c5dc93a3990ae7aace", + "entities": [ + { + "tableName": "rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`action` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `executions` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "action", + "columnName": "action", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "executions", + "columnName": "executions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "executions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`fileName` TEXT NOT NULL, `actionVerb` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "fileName", + "columnName": "fileName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actionVerb", + "columnName": "actionVerb", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b5ed230e3566b0c5dc93a3990ae7aace')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/co/adityarajput/fileflow/data/Converters.kt b/app/src/main/java/co/adityarajput/fileflow/data/Converters.kt index 7bb74c3..7de5702 100644 --- a/app/src/main/java/co/adityarajput/fileflow/data/Converters.kt +++ b/app/src/main/java/co/adityarajput/fileflow/data/Converters.kt @@ -2,11 +2,16 @@ package co.adityarajput.fileflow.data import androidx.room.TypeConverter import co.adityarajput.fileflow.data.models.Action +import kotlinx.serialization.json.Json class Converters { @TypeConverter - fun fromAction(action: Action) = action.toString() + fun fromAction(action: Action) = Json.encodeToString(action) @TypeConverter - fun toAction(value: String) = Action.fromString(value) + fun toAction(value: String) = try { + Json.decodeFromString (value) + } catch (_: Exception) { + Action.MOVE("", "", "", "") + } } diff --git a/app/src/main/java/co/adityarajput/fileflow/data/FileFlowDatabase.kt b/app/src/main/java/co/adityarajput/fileflow/data/FileFlowDatabase.kt index 41c9ba0..51d8fc9 100644 --- a/app/src/main/java/co/adityarajput/fileflow/data/FileFlowDatabase.kt +++ b/app/src/main/java/co/adityarajput/fileflow/data/FileFlowDatabase.kt @@ -1,14 +1,11 @@ package co.adityarajput.fileflow.data import android.content.Context -import androidx.room.Database -import androidx.room.Room -import androidx.room.RoomDatabase -import androidx.room.TypeConverters +import androidx.room.* import co.adityarajput.fileflow.data.models.Execution import co.adityarajput.fileflow.data.models.Rule -@Database([Rule::class, Execution::class], version = 1) +@Database([Rule::class, Execution::class], version = 2, autoMigrations = [AutoMigration(1, 2)]) @TypeConverters(Converters::class) abstract class FileFlowDatabase : RoomDatabase() { abstract fun ruleDao(): RuleDao diff --git a/app/src/main/java/co/adityarajput/fileflow/data/models/Action.kt b/app/src/main/java/co/adityarajput/fileflow/data/models/Action.kt index 8c9f89e..adec208 100644 --- a/app/src/main/java/co/adityarajput/fileflow/data/models/Action.kt +++ b/app/src/main/java/co/adityarajput/fileflow/data/models/Action.kt @@ -6,7 +6,7 @@ import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle import co.adityarajput.fileflow.R -import co.adityarajput.fileflow.utils.Logger +import co.adityarajput.fileflow.utils.FileSuperlative import co.adityarajput.fileflow.utils.getGetDirectoryFromUri import kotlinx.serialization.Serializable @@ -20,11 +20,8 @@ sealed class Action(val title: String) { val destFileNameTemplate: String, val keepOriginal: Boolean = true, val overwriteExisting: Boolean = false, - ) : Action(srcFileNamePattern) { - override fun toString() = when (this) { - is MOVE -> "MOVE\n$src\n$srcFileNamePattern\n$dest\n$destFileNameTemplate\n$keepOriginal\n$overwriteExisting" - } - } + val superlative: FileSuperlative = FileSuperlative.LATEST, + ) : Action(srcFileNamePattern) val verb get() = when (this) { @@ -44,19 +41,4 @@ sealed class Action(val title: String) { append(destFileNameTemplate) } } - - companion object { - fun fromString(value: String) = when { - value.startsWith("MOVE") -> { - val args = value.split("\n") - - MOVE(args[1], args[2], args[3], args[4], args[5].toBoolean(), args[6].toBoolean()) - } - - else -> { - Logger.e("Action", value) - throw IllegalArgumentException("Can't convert value to Action, unknown value: $value") - } - } - } } diff --git a/app/src/main/java/co/adityarajput/fileflow/services/FlowExecutor.kt b/app/src/main/java/co/adityarajput/fileflow/services/FlowExecutor.kt index 70adf7f..d15eb59 100644 --- a/app/src/main/java/co/adityarajput/fileflow/services/FlowExecutor.kt +++ b/app/src/main/java/co/adityarajput/fileflow/services/FlowExecutor.kt @@ -28,75 +28,68 @@ class FlowExecutor(private val context: Context) { continue } - for (srcFile in context.pathToFile(rule.action.src)?.listFiles() ?: arrayOf()) { - if (!srcFile.isFile || srcFile.name == null || !regex.matches(srcFile.name!!)) continue + val srcFile = context.pathToFile(rule.action.src)?.listFiles() + ?.filter { it.isFile && it.name != null && regex.matches(it.name!!) } + ?.maxByOrNull(rule.action.superlative.selector) + ?: continue + + val destFileName = regex.replace(srcFile.name!!, rule.action.destFileNameTemplate) + var destFile = destDir.listFiles().firstOrNull { it.isFile && it.name == destFileName } + + if (destFile != null) { + if (!rule.action.overwriteExisting) { + Logger.e("FlowExecutor", "${destFile.name} already exists") + continue + } resolver.openInputStream(srcFile.uri).use { src -> - if (src == null) { - Logger.e("FlowExecutor", "Failed to open ${srcFile.name}") - continue - } - - val destFileName = regex.replace( - srcFile.name!!, - rule.action.destFileNameTemplate, - ) - var destFile = destDir.listFiles() - .filter { it.isFile } - .firstOrNull { it.name == destFileName } - - if (destFile != null) { - if (!rule.action.overwriteExisting) { - Logger.e("FlowExecutor", "$destFileName already exists") + resolver.openInputStream(destFile.uri).use { dest -> + if (src == null || dest == null) { + Logger.e("FlowExecutor", "Failed to open file(s)") continue } - resolver.openInputStream(destFile.uri).use { dest -> - if (dest == null) { - Logger.e("FlowExecutor", "Failed to open existing destination file") - continue - } - - if (src.readBytes().contentEquals(dest.readBytes())) { - Logger.i( - "FlowExecutor", - "Source and destination files are identical", - ) - continue - } + if (src.readBytes().contentEquals(dest.readBytes())) { + Logger.i( + "FlowExecutor", + "Source and destination files are identical", + ) + continue } - - Logger.i("FlowExecutor", "Deleting existing $destFileName") - destFile.delete() } + } - destFile = destDir.createFile( - srcFile.type ?: "application/octet-stream", - destFileName, - ) + Logger.i("FlowExecutor", "Deleting existing ${destFile.name}") + destFile.delete() + } + + destFile = destDir.createFile( + srcFile.type ?: "application/octet-stream", + destFileName, + ) + + if (destFile == null) { + Logger.e("FlowExecutor", "Failed to create $destFileName") + continue + } - if (destFile == null) { - Logger.e("FlowExecutor", "Failed to create $destFileName") + resolver.openInputStream(srcFile.uri).use { src -> + resolver.openOutputStream(destFile.uri).use { dest -> + if (src == null || dest == null) { + Logger.e("FlowExecutor", "Failed to open file(s)") continue } - resolver.openOutputStream(destFile.uri).use { dest -> - if (dest == null) { - Logger.e("FlowExecutor", "Failed to open $destFileName") - continue - } - - Logger.i("FlowExecutor", "Copying ${srcFile.name} to ${destFile.name}") - src.copyTo(dest) - repository.registerExecution( - rule, - Execution(srcFile.name!!, rule.action.verb), - ) + Logger.i("FlowExecutor", "Copying ${srcFile.name} to ${destFile.name}") + src.copyTo(dest) + repository.registerExecution( + rule, + Execution(srcFile.name!!, rule.action.verb), + ) - if (!rule.action.keepOriginal) { - Logger.i("FlowExecutor", "Deleting original ${srcFile.name}") - srcFile.delete() - } + if (!rule.action.keepOriginal) { + Logger.i("FlowExecutor", "Deleting original ${srcFile.name}") + srcFile.delete() } } } diff --git a/app/src/main/java/co/adityarajput/fileflow/utils/Files.kt b/app/src/main/java/co/adityarajput/fileflow/utils/Files.kt index 9955409..20d72fd 100644 --- a/app/src/main/java/co/adityarajput/fileflow/utils/Files.kt +++ b/app/src/main/java/co/adityarajput/fileflow/utils/Files.kt @@ -3,5 +3,13 @@ package co.adityarajput.fileflow.utils import android.content.Context import androidx.core.net.toUri import androidx.documentfile.provider.DocumentFile +import co.adityarajput.fileflow.R fun Context.pathToFile(path: String) = DocumentFile.fromTreeUri(this, path.toUri()) + +enum class FileSuperlative(val displayName: Int, val selector: (DocumentFile) -> Long) { + EARLIEST(R.string.earliest, { -it.lastModified() }), + LATEST(R.string.latest, { it.lastModified() }), + SMALLEST(R.string.smallest, { -it.length() }), + LARGEST(R.string.largest, { it.length() }), +} diff --git a/app/src/main/java/co/adityarajput/fileflow/viewmodels/UpsertRuleViewModel.kt b/app/src/main/java/co/adityarajput/fileflow/viewmodels/UpsertRuleViewModel.kt index 313ab0c..5d594c6 100644 --- a/app/src/main/java/co/adityarajput/fileflow/viewmodels/UpsertRuleViewModel.kt +++ b/app/src/main/java/co/adityarajput/fileflow/viewmodels/UpsertRuleViewModel.kt @@ -8,6 +8,7 @@ import androidx.lifecycle.ViewModel import co.adityarajput.fileflow.data.Repository import co.adityarajput.fileflow.data.models.Action import co.adityarajput.fileflow.data.models.Rule +import co.adityarajput.fileflow.utils.FileSuperlative import co.adityarajput.fileflow.utils.Logger import co.adityarajput.fileflow.utils.pathToFile @@ -26,19 +27,20 @@ class UpsertRuleViewModel( val srcFileNamePattern: String = "", val dest: String = "", val destFileNameTemplate: String = "", + val superlative: FileSuperlative = FileSuperlative.LATEST, val keepOriginal: Boolean = true, val overwriteExisting: Boolean = false, ) { constructor(rule: Rule) : this( rule.id, (rule.action as Action.MOVE).src, rule.action.srcFileNamePattern, - rule.action.dest, rule.action.destFileNameTemplate, rule.action.keepOriginal, - rule.action.overwriteExisting, + rule.action.dest, rule.action.destFileNameTemplate, rule.action.superlative, + rule.action.keepOriginal, rule.action.overwriteExisting, ) fun toRule() = Rule( Action.MOVE( - src, srcFileNamePattern, dest, destFileNameTemplate, keepOriginal, - overwriteExisting, + src, srcFileNamePattern, dest, destFileNameTemplate, + keepOriginal, overwriteExisting, superlative, ), id = ruleId, ) diff --git a/app/src/main/java/co/adityarajput/fileflow/views/screens/UpsertRuleScreen.kt b/app/src/main/java/co/adityarajput/fileflow/views/screens/UpsertRuleScreen.kt index f2035d5..16c05a0 100644 --- a/app/src/main/java/co/adityarajput/fileflow/views/screens/UpsertRuleScreen.kt +++ b/app/src/main/java/co/adityarajput/fileflow/views/screens/UpsertRuleScreen.kt @@ -6,9 +6,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.selection.toggleable import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -20,6 +18,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDecoration import androidx.lifecycle.viewmodel.compose.viewModel import co.adityarajput.fileflow.R +import co.adityarajput.fileflow.utils.FileSuperlative import co.adityarajput.fileflow.utils.getGetDirectoryFromUri import co.adityarajput.fileflow.utils.takePersistablePermission import co.adityarajput.fileflow.viewmodels.FormError @@ -115,6 +114,7 @@ fun UpsertRuleScreen( private fun ColumnScope.ActionPage(viewModel: UpsertRuleViewModel) { val context = LocalContext.current + var superlativeDropdownExpanded by remember { mutableStateOf(false) } val filesInSrc = remember(viewModel.state.values.src) { viewModel.getFilesInSrc(context) } val showWarning = remember(viewModel.state.values) { if (viewModel.state.error != null || filesInSrc == null || filesInSrc.isEmpty()) false @@ -155,7 +155,7 @@ private fun ColumnScope.ActionPage(viewModel: UpsertRuleViewModel) { Modifier .fillMaxWidth() .clickable { srcPicker.launch(null) }, - style = MaterialTheme.typography.bodyMedium, + style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Normal, ) OutlinedTextField( @@ -202,6 +202,30 @@ private fun ColumnScope.ActionPage(viewModel: UpsertRuleViewModel) { fontWeight = FontWeight.Normal, ) } + Box { + Text( + buildAnnotatedString { + append(stringResource(R.string.choose_superlative)) + withStyle(SpanStyle(textDecoration = TextDecoration.Underline)) { + append(stringResource(viewModel.state.values.superlative.displayName)) + } + }, + Modifier.clickable { superlativeDropdownExpanded = true }, + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Normal, + ) + DropdownMenu(superlativeDropdownExpanded, { superlativeDropdownExpanded = false }) { + FileSuperlative.entries.forEach { + DropdownMenuItem( + { Text(stringResource(it.displayName)) }, + { + viewModel.updateForm(viewModel.state.values.copy(superlative = it)) + superlativeDropdownExpanded = false + }, + ) + } + } + } Icon( painterResource(R.drawable.arrow_down), stringResource(R.string.alttext_arrow_down), @@ -220,7 +244,7 @@ private fun ColumnScope.ActionPage(viewModel: UpsertRuleViewModel) { Modifier .fillMaxWidth() .clickable { destPicker.launch(null) }, - style = MaterialTheme.typography.bodyMedium, + style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Normal, ) OutlinedTextField( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b3f5247..6034b84 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -31,6 +31,11 @@ Pattern should match %1$s " or " Delete original files from the source after execution +"In case of multiple matches, choose: " +earliest +latest +smallest +largest "Destination: " File name template Enter a regex template From 2a05a9807b19ef070aedd36932a13277b0cd4ab5 Mon Sep 17 00:00:00 2001 From: Aditya RajputDate: Sat, 7 Feb 2026 21:20:17 +0530 Subject: [PATCH 4/6] Disable backups in debug builds --- app/build.gradle.kts | 3 +++ app/src/main/AndroidManifest.xml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8393446..715f19d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -31,6 +31,7 @@ android { debug { applicationIdSuffix = ".debug" resValue("string", "app_name_launcher", "FileFlow Debug") + manifestPlaceholders["allowBackup"] = false } create("nightly") { isDebuggable = false @@ -42,6 +43,7 @@ android { ) applicationIdSuffix = ".nightly" resValue("string", "app_name_launcher", "FileFlow Nightly") + manifestPlaceholders["allowBackup"] = true } release { isDebuggable = false @@ -51,6 +53,7 @@ android { getDefaultProguardFile("proguard-android-optimize.txt"), "proguard/release.pro", ) + manifestPlaceholders["allowBackup"] = true } } buildFeatures { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7d73135..d5ba3e7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,7 +5,7 @@ Date: Sat, 7 Feb 2026 22:33:42 +0530 Subject: [PATCH 5/6] Execute template on upsert screen --- .../viewmodels/UpsertRuleViewModel.kt | 53 +++++++++++++------ .../views/screens/UpsertRuleScreen.kt | 46 ++++++++-------- app/src/main/res/values/strings.xml | 2 + 3 files changed, 61 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/co/adityarajput/fileflow/viewmodels/UpsertRuleViewModel.kt b/app/src/main/java/co/adityarajput/fileflow/viewmodels/UpsertRuleViewModel.kt index 5d594c6..72b7f6d 100644 --- a/app/src/main/java/co/adityarajput/fileflow/viewmodels/UpsertRuleViewModel.kt +++ b/app/src/main/java/co/adityarajput/fileflow/viewmodels/UpsertRuleViewModel.kt @@ -19,6 +19,7 @@ class UpsertRuleViewModel( data class State( val values: Values = Values(), val error: FormError? = null, + val warning: FormWarning? = null, ) data class Values( @@ -30,6 +31,8 @@ class UpsertRuleViewModel( val superlative: FileSuperlative = FileSuperlative.LATEST, val keepOriginal: Boolean = true, val overwriteExisting: Boolean = false, + val currentSrcFileNames: List ? = null, + val predictedDestFileNames: List ? = null, ) { constructor(rule: Rule) : this( rule.id, (rule.action as Action.MOVE).src, rule.action.srcFileNamePattern, @@ -51,25 +54,38 @@ class UpsertRuleViewModel( else State(Values(rule), null), ) - fun getFilesInSrc(context: Context): List ? { + fun updateForm(context: Context, values: Values) { + var currentSrcFileNames: List ? = null try { - if (state.values.src.isBlank()) return null - - return context.pathToFile(state.values.src)!! - .listFiles() - .filter { it.isFile && it.name != null } - .map { it.name!! } + if (values.src.isNotBlank()) + currentSrcFileNames = context.pathToFile(values.src)!!.listFiles() + .filter { it.isFile && it.name != null }.map { it.name!! } } catch (e: Exception) { - Logger.e("UpsertRuleViewModel", "Couldn't fetch files in ${state.values.src}", e) - return null + Logger.e("UpsertRuleViewModel", "Couldn't fetch files in ${values.src}", e) } - } - fun updateForm(values: Values) { - state = State(values, getError(values)) + var predictedDestFileNames: List ? = null + var warning: FormWarning? = null + try { + val regex = Regex(values.srcFileNamePattern) + + if (values.destFileNameTemplate.isNotBlank()) + predictedDestFileNames = currentSrcFileNames + ?.filter { regex.matches(it) } + ?.also { if (it.isEmpty()) warning = FormWarning.NO_MATCHES_IN_SRC } + ?.map { regex.replace(it, values.destFileNameTemplate) } + ?.distinct() + } catch (_: Exception) { + } + + val values = values.copy( + currentSrcFileNames = currentSrcFileNames, + predictedDestFileNames = predictedDestFileNames, + ) + state = State(values, getError(values), warning) } - private fun getError(values: Values = state.values): FormError? { + private fun getError(values: Values): FormError? { try { if ( values.src.isBlank() || @@ -79,18 +95,20 @@ class UpsertRuleViewModel( ) return FormError.BLANK_FIELDS Regex(values.srcFileNamePattern).pattern == values.srcFileNamePattern + + if (values.predictedDestFileNames == null) return FormError.INVALID_TEMPLATE } catch (_: Exception) { - Logger.d("RulesViewModel", "Invalid regex") + Logger.d("UpsertRuleViewModel", "Invalid regex") return FormError.INVALID_REGEX } return null } suspend fun submitForm() { - if (getError() == null) { + if (getError(state.values) == null) { val rule = state.values.toRule() Logger.d( - "RulesViewModel", + "UpsertRuleViewModel", "${if (state.values.ruleId == 0) "Adding" else "Updating"} $rule", ) repository.upsert(rule) @@ -98,4 +116,5 @@ class UpsertRuleViewModel( } } -enum class FormError { BLANK_FIELDS, INVALID_REGEX } +enum class FormError { BLANK_FIELDS, INVALID_REGEX, INVALID_TEMPLATE } +enum class FormWarning { NO_MATCHES_IN_SRC } diff --git a/app/src/main/java/co/adityarajput/fileflow/views/screens/UpsertRuleScreen.kt b/app/src/main/java/co/adityarajput/fileflow/views/screens/UpsertRuleScreen.kt index 16c05a0..cf2665b 100644 --- a/app/src/main/java/co/adityarajput/fileflow/views/screens/UpsertRuleScreen.kt +++ b/app/src/main/java/co/adityarajput/fileflow/views/screens/UpsertRuleScreen.kt @@ -22,6 +22,7 @@ import co.adityarajput.fileflow.utils.FileSuperlative import co.adityarajput.fileflow.utils.getGetDirectoryFromUri import co.adityarajput.fileflow.utils.takePersistablePermission import co.adityarajput.fileflow.viewmodels.FormError +import co.adityarajput.fileflow.viewmodels.FormWarning import co.adityarajput.fileflow.viewmodels.Provider import co.adityarajput.fileflow.viewmodels.UpsertRuleViewModel import co.adityarajput.fileflow.views.components.AppBar @@ -115,11 +116,6 @@ private fun ColumnScope.ActionPage(viewModel: UpsertRuleViewModel) { val context = LocalContext.current var superlativeDropdownExpanded by remember { mutableStateOf(false) } - val filesInSrc = remember(viewModel.state.values.src) { viewModel.getFilesInSrc(context) } - val showWarning = remember(viewModel.state.values) { - if (viewModel.state.error != null || filesInSrc == null || filesInSrc.isEmpty()) false - else filesInSrc.none { Regex(viewModel.state.values.srcFileNamePattern).matches(it) } - } val srcPicker = rememberLauncherForActivityResult( @@ -127,9 +123,7 @@ private fun ColumnScope.ActionPage(viewModel: UpsertRuleViewModel) { ) { uri -> uri ?: return@rememberLauncherForActivityResult context.takePersistablePermission(uri) - viewModel.updateForm( - values = viewModel.state.values.copy(src = uri.toString()), - ) + viewModel.updateForm(context, viewModel.state.values.copy(src = uri.toString())) } val destPicker = rememberLauncherForActivityResult( @@ -137,9 +131,7 @@ private fun ColumnScope.ActionPage(viewModel: UpsertRuleViewModel) { ) { uri -> uri ?: return@rememberLauncherForActivityResult context.takePersistablePermission(uri) - viewModel.updateForm( - values = viewModel.state.values.copy(dest = uri.toString()), - ) + viewModel.updateForm(context, viewModel.state.values.copy(dest = uri.toString())) } Text( @@ -161,9 +153,7 @@ private fun ColumnScope.ActionPage(viewModel: UpsertRuleViewModel) { OutlinedTextField( viewModel.state.values.srcFileNamePattern, { - viewModel.updateForm( - viewModel.state.values.copy(srcFileNamePattern = it), - ) + viewModel.updateForm(context, viewModel.state.values.copy(srcFileNamePattern = it)) }, Modifier.fillMaxWidth(), label = { @@ -171,13 +161,14 @@ private fun ColumnScope.ActionPage(viewModel: UpsertRuleViewModel) { }, placeholder = { Text(stringResource(R.string.pattern_placeholder)) }, supportingText = { - if (filesInSrc == null || filesInSrc.isEmpty()) + if (viewModel.state.values.currentSrcFileNames?.isEmpty() ?: true) Text(stringResource(R.string.match_entire_filename)) else Text( stringResource( R.string.pattern_should_match, - filesInSrc.joinToString(stringResource(R.string.or), limit = 3), + viewModel.state.values.currentSrcFileNames!! + .joinToString(stringResource(R.string.or), limit = 3), ), ) }, @@ -190,7 +181,7 @@ private fun ColumnScope.ActionPage(viewModel: UpsertRuleViewModel) { ) Row( Modifier.toggleable(!viewModel.state.values.keepOriginal) { - viewModel.updateForm(viewModel.state.values.copy(keepOriginal = !it)) + viewModel.updateForm(context, viewModel.state.values.copy(keepOriginal = !it)) }, Arrangement.spacedBy(dimensionResource(R.dimen.padding_small)), Alignment.Top, @@ -219,7 +210,7 @@ private fun ColumnScope.ActionPage(viewModel: UpsertRuleViewModel) { DropdownMenuItem( { Text(stringResource(it.displayName)) }, { - viewModel.updateForm(viewModel.state.values.copy(superlative = it)) + viewModel.updateForm(context, viewModel.state.values.copy(superlative = it)) superlativeDropdownExpanded = false }, ) @@ -250,15 +241,23 @@ private fun ColumnScope.ActionPage(viewModel: UpsertRuleViewModel) { OutlinedTextField( viewModel.state.values.destFileNameTemplate, { - viewModel.updateForm( - viewModel.state.values.copy(destFileNameTemplate = it), - ) + viewModel.updateForm(context, viewModel.state.values.copy(destFileNameTemplate = it)) }, Modifier.fillMaxWidth(), label = { Text(stringResource(R.string.file_name_template)) }, placeholder = { Text(stringResource(R.string.template_placeholder)) }, + supportingText = { + if (viewModel.state.values.predictedDestFileNames?.isNotEmpty() ?: false) + Text( + stringResource( + R.string.template_will_yield, + viewModel.state.values.predictedDestFileNames!! + .joinToString(stringResource(R.string.or), limit = 3), + ), + ) + }, colors = OutlinedTextFieldDefaults.colors( focusedContainerColor = MaterialTheme.colorScheme.secondaryContainer, unfocusedContainerColor = MaterialTheme.colorScheme.secondaryContainer, @@ -268,7 +267,7 @@ private fun ColumnScope.ActionPage(viewModel: UpsertRuleViewModel) { ) Row( Modifier.toggleable(viewModel.state.values.overwriteExisting) { - viewModel.updateForm(viewModel.state.values.copy(overwriteExisting = it)) + viewModel.updateForm(context, viewModel.state.values.copy(overwriteExisting = it)) }, Arrangement.spacedBy(dimensionResource(R.dimen.padding_small)), Alignment.Top, @@ -294,5 +293,6 @@ private fun ColumnScope.ActionPage(viewModel: UpsertRuleViewModel) { fontWeight = FontWeight.Normal, ) if (viewModel.state.error == FormError.INVALID_REGEX) ErrorText(R.string.invalid_regex) - if (showWarning) WarningText(R.string.pattern_doesnt_match_src_files) + else if (viewModel.state.error == FormError.INVALID_TEMPLATE) ErrorText(R.string.invalid_template) + else if (viewModel.state.warning == FormWarning.NO_MATCHES_IN_SRC) WarningText(R.string.pattern_doesnt_match_src_files) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6034b84..5061ece 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -39,9 +39,11 @@ "Destination: " File name template Enter a regex template +Template will yield %1$s Overwrite existing files in the destination, in case of conflict here↗ Regex pattern contains errors +Regex template contains errors Regex pattern doesn\'t match any file in the source folder Cancel Add From 6b9ae5ad5cf3ea6f3a157e0626a82abc1d62625d Mon Sep 17 00:00:00 2001 From: Aditya RajputDate: Sun, 8 Feb 2026 02:51:45 +0530 Subject: [PATCH 6/6] Show a toast if manual execution fails --- .../fileflow/viewmodels/RulesViewModel.kt | 12 +++++++++++- .../fileflow/views/components/ManageRuleDialog.kt | 5 ++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/co/adityarajput/fileflow/viewmodels/RulesViewModel.kt b/app/src/main/java/co/adityarajput/fileflow/viewmodels/RulesViewModel.kt index 8fe6cf1..abc4e1a 100644 --- a/app/src/main/java/co/adityarajput/fileflow/viewmodels/RulesViewModel.kt +++ b/app/src/main/java/co/adityarajput/fileflow/viewmodels/RulesViewModel.kt @@ -27,9 +27,19 @@ class RulesViewModel(private val repository: Repository) : ViewModel() { var selectedRule by mutableStateOf (null) - fun executeRule(context: Context) { + fun executeRule(context: Context, showToast: (String) -> Unit) { viewModelScope.launch { + val latestLogBeforeExecution = Logger.logs.lastOrNull() + FlowExecutor(context).run(listOf(selectedRule!!)) + + val recentErrorLog = Logger.logs + .dropWhile { it != latestLogBeforeExecution }.drop(1) + .firstOrNull { it.contains("[ERROR]") } + ?: Logger.logs.lastOrNull { it.contains("[ERROR]") } + if (recentErrorLog != null) { + showToast("Error:" + recentErrorLog.substringAfter("[ERROR]")) + } } } diff --git a/app/src/main/java/co/adityarajput/fileflow/views/components/ManageRuleDialog.kt b/app/src/main/java/co/adityarajput/fileflow/views/components/ManageRuleDialog.kt index 3c7a4f3..3012c0e 100644 --- a/app/src/main/java/co/adityarajput/fileflow/views/components/ManageRuleDialog.kt +++ b/app/src/main/java/co/adityarajput/fileflow/views/components/ManageRuleDialog.kt @@ -1,5 +1,6 @@ package co.adityarajput.fileflow.views.components +import android.widget.Toast import androidx.compose.foundation.layout.Row import androidx.compose.material3.* import androidx.compose.runtime.Composable @@ -57,9 +58,11 @@ fun ManageRuleDialog(viewModel: RulesViewModel) { TextButton( { when (dialogState) { - DialogState.EXECUTE -> viewModel.executeRule(context) DialogState.TOGGLE_RULE -> viewModel.toggleRule() DialogState.DELETE -> viewModel.deleteRule() + DialogState.EXECUTE -> viewModel.executeRule(context) { + Toast.makeText(context, it, Toast.LENGTH_LONG).show() + } } hideDialog() },