From 2edfe24362c4293525680297be37240fbb5b3617 Mon Sep 17 00:00:00 2001 From: Abdo Date: Thu, 21 Sep 2023 23:42:25 +0300 Subject: [PATCH] Add interface and app data folder detection --- README.md | 5 +- ankiweb_page.html | 6 ++- designer/dialog.ui | 114 ++++++++++++++++++++++++++++++++++++++++ images/dialog.png | Bin 0 -> 11974 bytes requirements/bundle.in | 2 +- requirements/bundle.txt | 2 +- requirements/dev.txt | 4 +- src/__init__.py | 40 ++++++-------- src/ankiapp_importer.py | 5 +- src/appdata.py | 57 ++++++++++++++++++++ src/gui/__init__.py | 0 src/gui/dialog.py | 86 ++++++++++++++++++++++++++++++ 12 files changed, 286 insertions(+), 35 deletions(-) create mode 100644 designer/dialog.ui create mode 100644 images/dialog.png create mode 100644 src/appdata.py create mode 100644 src/gui/__init__.py create mode 100644 src/gui/dialog.py diff --git a/README.md b/README.md index a3175ee..afc42f7 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,13 @@ So you can no longer export a zip of your cards [without paying](https://www.ank This add-on salvages the cards from the SQLite database and was inspired by the Reddit post linked above. It can import cards, decks, note types, and media files. +![The add-on's dialog](images/dialog.png) + ## How to Use - Download the add-on from https://ankiweb.net/shared/info/2072125761 - Make sure all your AnkiApp decks are downloaded before using the add-on. For that, go to AnkiApp, click on each of your decks, then click on the Download button at the bottom if it's shown. -- Run Anki and go to **Tools > Import From AnkiApp** and choose AnkiApp's database file (`C:\Users\%USERNAME%\AppData\Roaming\AnkiApp\databases\file__0` on Windows; `~/Library/Application Support/AnkiApp/databases/file__0/1` or under `~/Library/Containers/com.ankiapp.client/Data/Documents/ankiapp` on macOS). -- Grab a cup of coffee while waiting for importing to finish. +- Run Anki and go to **Tools > Import From AnkiApp**. The add-on tries to detect AnkiApp's data folder on your system automatically. If you see the "Data folder" field already populated, you can go ahead and click Import. ## Notes & Known Issues diff --git a/ankiweb_page.html b/ankiweb_page.html index 56a4415..4b21318 100644 --- a/ankiweb_page.html +++ b/ankiweb_page.html @@ -4,8 +4,10 @@ How to Use - - + Notes & Known Issues diff --git a/designer/dialog.ui b/designer/dialog.ui new file mode 100644 index 0000000..d58c773 --- /dev/null +++ b/designer/dialog.ui @@ -0,0 +1,114 @@ + + + Dialog + + + + 0 + 0 + 500 + 200 + + + + + 0 + 0 + + + + Dialog + + + + + + + 0 + 0 + + + + + + + + + + AnkiApp data folder + + + Data folder + + + true + + + + + + + + + + + + ... + + + + + + + + + AnkiApp's SQLite database file. If you only have access to a single database file, you can use this, otherwise it's recommended to specify the data folder instead to allow the add-on to import more data + + + Database file + + + + + + + + + false + + + + + + + false + + + ... + + + + + + + + + + + + Import + + + + + + + Try to download missing media from AnkiApp servers + + + + + + + + diff --git a/images/dialog.png b/images/dialog.png new file mode 100644 index 0000000000000000000000000000000000000000..40b95cdd342877c6db3c2781f6176917a0840b19 GIT binary patch literal 11974 zcmbVyXH-*Nw{B1YK>=Su5s<2YfE20Hn+PJkN*5*cB3(*|qJoGt=`|n_dWX=1ib@S2 z0RjZbs}KlXAP_?!+|7Hwd(StH>#F-fBGBI*8fe) zAP{r!>4&DzuiOO$lCXPlPt)SLHD9if8e^w? z+DV-T=gQ;XrWghg=tWy)U*3(g^AW&hwwF@C-DHq6U_Kws7d8+`KHEm#x<7wKSW4X=eEmaoebBYLYod78h%)og4*{Gg(ex*OlZZ41qqFgfT|RerYd`8T@80`5|yb zQ8$K(OGrDGxuvdYMCsx8Bwoo>A;S-TkaasV2M2L;q4y!?G$4>gZSPIJuxE3C$q3Q5msbGyuC?i_C*fqdm|ijUkm}W#*|g|2%O}F$v_8dYbAp|k$alX zo@0fYHk$e;ymy|we$OeVp9oVCbSk*a5lq#Q4&AcnYW!u}wJb8zDHQpwBS$R3Pqtqb zZC~a#n}o-|puQPZmk{q81vjjp0fD-)PU?Ffjz&)!Z%dhYsB^;?**+FZO-@Y-)zsAN z4hX~g<5-2g&0#U3KEJAzh(*Ia&*~$Q6y7Bmbrtq2R)qD%R@>vwuJF&lYJZlS_&~*Q z`JHAw?t$}_X?xqjT3Ui2(0StUvcggi6@Mu4a`1xLzf7p??shoX#`$Sa9&BRqo2|Hv z`OeTGdfrIBZR!^9Qm7x&yEkL%k;mA(r^}Z60w}036G{Nry{3g}4H{I10^`!UL|1uJ zQ~wGZJS`O6j*NP~KI(GNC58{}q2zb%eCOKT>f9du^zy8aPdak9)8y*#Ut#2QXgYVD zH+!~>oofgGs%<7-J+s?S1?(g#$-=0uRDgM(X=l#*l!|djKi)bjj(M}HWVk3NG%W6S z{uZ==j9nhIfcmM4RIGAhp6}W7<5J=piJ0YX%=>J)8qB#>*wzYy1q3?IRyPr3Fsmd; zS(=;QIFbIF1z+=XKVUu>Ga#60FQA*mla6tMIskUN^$mEycHN+wM;EHA zb-fNfH8eEqlPMCQt5gz(K7O+fY!{^_&$5(AbAfpJlb=F1&(>iBSc=i$_Y6rqxv4he zBx1+dWx!A!F`DD);gJG|3n}QdS+LGo%{X^bE#>9nZ+Cx#YG*Mpa@p%0ZeoS*8l-t3 zHTLFDkXA@+mu&`hYQO(4FDjJUTJaNDE=lsnX(H@1H244MzjqjE#`aFtL$da=y3Yp# zd79sXe)(qx(+k`rlxr9~Y|TsM%t@0&OBW=boxK4+3o3-|C~T5*kKUO+%gT{*j((pP znJo*EeiXyP!`HG%H>9g;Jdlt|-``|>oDaDjjby%l_iy-l8EY%5`$q%n4_nO5{WKM~ z4)93pQ)+M?2j<*sgGc?Agd@o`59g+L*R2>!^G-s@?KLd{b%mVDAkbmMTtrF0c9E#X zXJ`G~-wR>XI=pbpS#bFNr3#rk4ms_T<#pqwA;}&Cg|lm{<2M}ke8?lZqy1VZanLPK zkE3$OT0`6oHZZK~XhjUa(kSpJ3jSyhmenrekb6$@_vCpUX0bQP#}M!+n42&nA|ALq1tRuL_-`wPAT9?rTG@beA(pdXZCM>!HXh9FP& z2EBH_l@It1F6f9@kvz54!=-Cd z8Zn92#oZEANB0Re<=MbqH{gx}=It~@iqI&oLs`BE6nSlP9x0a|A;XEwY^3b@GiX<|6NG>|NKMRO@rW&kd9EGU&li) z4!L|cBR3U#0`lLS4${&@g?Yg8Fb4)$CxzSBWZjb?Oh5>!tgN)h{i*F*t^M^SZR#F` zN?wt69lpk)^8A_U1)wRx!&W5dkm#Wp`KSKy2(tuv*7n9*x>Xl*x#eV@t9E$yWoc{kDlz*S5e;@Ww|29Bm zZnelqLPmLnjxly_#ejQ8d3X7_xw&guK!ukt@~AwDVdfUSasPd?<}12(=1@_cwIqVBCetXe*djW;r}6NZ)us%bp|lM%qYLHFTC5GM>V|p zBCpyUQ`|wOWL}y=C@f_ZNeF@*BI8H7Ze<#A^2PVaJ)%C|0AUn*9J%4MSS%iPLWbdA zv8W{%>n05jqzX|vwPP-(oT3Pc4uU{Car|LvN5|}e?kePPwTu?h4gx_^V=4i3G4Ps! z)gJbk_qqz;b3Mj`^BC&^;vRkfbjpslt{(=`W3Hw+F2aI*W^3i3{HV75rlX<59p}mo zk^#4RlzmQ4PDw+9=|b2Fx1G!{T9MN278oDAiMnDgmVE(@(i;gLzhlYksh7q7KV*;eX*5)rOd%2cGB_wS$D-43=totWVV(lx_z)gv@{spng1L)Q4DGW$$2D-9Dl0_DZYD425=Q=XVUU zt~s;`jX|k4Mtl0%YljDrh!W>S$>b1*cO5kuxzIpbpWXo4Z}rlzMM|#we@FEk!SSwG z{PEnk(xJUg);-aA9j)n(AP3h0D`A)_leF-U75H_E?h1zqJh?F=j;owzxR}bG< zF6vlT#lw7Iu03$`A#R1Pvf*jp?8)Bo6|H)5W9>eF_T(S?rI#(wn@W6hBJ3D8;)<+n zxAwl2hpF=KYE6C5mAsHCKNzSQUO-rtUa~>XB$Boti9QUSgYjKj62<6fO?3RpX!od) zmAG~DlaN$!scfk1(BQ{9lMerrbLlO&DZCm^rU)s#D)kezsXctXrgiBXyHk+E6P%a2 zI^-a2S6!=!&%7>Eut}QUD&iZ#!3Ud~Qt<4mUa@zoWlMI|K^>qS+$EW?>?6ma`y%PtM$IGD>ts}COg_9<5LtBH4L$C7^XqKJ9{Zr#41Xs@?=E5B*qnOIQm7zim$hcN zTsLGtFX=NOrW^QVoU~xs_VBVpUsHGfhDXjOxPn2Yr9$dSnT&8J_l0Or+Qz;JtpeGi z;o{zcG{MzbPznVtGcBaNRv>kZ$u7OUN0m<+**3PM2yY(|q!^-%2S&r$(jBE?4dLmp-mNxx{=c5;9K&NM5 zhNBM_s|BA}i3xglUi(ZbTt8ebRAuT&GM{%qyx9!e=9)W>vsvV@q15urID9x}MO4dj zk6vtDUzV&|g99dFMD4P6Ju``lW>J|=QSJ7j^TD+bHzGDyR-j7ZJCP{=1;fRId#wCh z&kIyLQNf*(?+qM8NaFKC;y$(5;zd-ro>k`#ytZr906!hpmI{-Ic&T^=?j|+FQ#j;l zmH*YNB~MgrJJ)8~G8AoVBNu2cdNapbv?+&GFYouYyFSl2zG&Ca)RN*=ih2#k(k&I- zjHXh4%HQUT^*+l~`b#UeV9e0^;FdGq#I*OLV5i=BEDSmNEARq@M0ejOGmG*`p37=bCvmYMUn%wngg1J%B^2RIp7XBcGtgS!xf4)$P2(F8$jxXv!Q;5B zeC5y%Bzd)M8%e3nmnSqZevXvmwj@2wZ*Me$!B*-rVqMZF*6MTZTwD(|{FpnIMWrBq z4+zgq%@k<2AN`%-lUosx?;3hp4ijlr)MlU|~gsM7ah{8`L#dQYf>n{(6JMyN*jDRujlT#sziq>PqB5>at93pugz?u;0`X zrg)X6gmb<0##i*nywF0Do(H;MN>3z;&_*Q2axHGPb{ULiH$}8i;=l8Zqrq>xkA!S| zUzwOlC2~$Bg!fW!XUN}~Li9u0p*yo)Qxz;5P>bgBrzd$Q{`QEu+$EOo!;CO5BmF(- z5KJt5o`u`%TE}9d@=`AD2=0SG*AxFNCJoKDc!5zlazJ!$fnl1<7B82Cx0BbysW&Il zU;-1i(BklS$FwWBF78L&hXWQ9A$FL8$?QSqHfb$9)vBogC$(-T0cJAS*Zy%t-UF|W zm+lHfcZJQ&%~SXL&;D>OU>zH6F zU4NM_MK|kk=~W1|qVA4`ZG}v(&FCYn(#D zsGXqLu9@)rvq_d#;_336-BSVI&sCW3c0(0zStW~Jl`}tn>sU%i0xA`%v+@)V83s^c zfV*KD)v}GQ;;$R``)Ax>n=$t&Rd915lVA&r!tsa_iSEZQB735qX=+wx<%oQ-P%n9@ z|0+G|CkT|Ty`3_iIi=_S+5Y-}VE#Fo)4CSwzXkn0%j>a3@{PkqvF|Y~| zDfjG+U{Qx2nobfR4O~gl+E|hHK^Nz@&i*OkQtMeAL~SloHk2)3YrSPOd)%`(BjYON zBy1&LIMXq_Vz@vRi-a3w3l!f^Xha!d5k5`IS4zwN6IVYjq0xfw2)tu20cwR-{xw%@ zxk}gNLN=zmu>2K`u@$%zDrtAh&TpW7r9cd*9ySUG)r9Q1qtcjjRl*wWqsVx~L1jzJ zB$G~s`=d!oc2FU7>?x8O^yr$8$8NNRAljrfUEa3Br&qiaHhDq@njtQG@>a)>(r;AAZKMx1l^^{!uz?IcH%txF<FfA82rYQ)Cl2&Ko9y#^P@OWbo)3wp-rJ{ASh-hP+P=&P#pr4{Uzku`xSsC>*mk z6F}*19oq23IMn;jscVA8SE-VCx9(1DGkeuzNMf&dXM#XH8rS273b`oO+R5khSN7In zAymXJ>vTgs@vJjZsu?LRmmjIX41KUk*v)%rZa4%%y+C}|_bUAtmUiqZ+=-lR9uSus z#=XC$g5vSWN?nDm=4b0#PVVN_XF9+qqy;=j1y-(PDuPrxbt>6$V)331l3=5&O~>>1 zH}k^A7kYJ(1rpj5yI4DvX|r$Qa0P2%p?F_*mDN@8T;avgmb9C50H$sUUj8~J0l{x; zh6kLag;TqKQmlNtqqtFO^K#uIk$V<3S-)l>T=rX@*^o{Khj_*w-D~MZzui!wGE5g9 zpqw!L-0Q^rm{dxsb8u7j`m~E{RL?|x6JT6;B&1FHfzQM9(UNM zu;z_74<4d8Z9*vC6}9r#E}Io%O@CVG!&?xJi0gX6h9s@rshAp^K*a(Ym5vh#EFTvm zC~Sh~YaJ%xxa%I<(g2Wc4g)VrVD3N~6bs2#1QS=A#TD^XY&Qc0d`;c2K#}dxC4JO- zYs_b?w$Gu$&cbK;DKmT&q2I~DFQ@O>?RgdRVCef?z&)l5g+shO->#3L4y5s`9Sg2M zyyV|)wnQa(`(p*3Q2@vvF3sU2Jk<5m8T;-00=)yqxGWn@&8iMnMhqby!R6Gpj=MZ( z8U^Lh{gBquZ-$wVPhN|Q8TQFw<-7B{8`tWkH%{t<*c^~NDnrUG@^fE*HqjgSLk1`>2&`QQKQpVw(QSMSfRrRv;Hr&^4 zkCi{D%Vu|sRqWgS^a?RNvJ_kLZAeiQZBUhJlW?KY?=`N;F66nKvl2D>{!B)}gO~b> zbvtq&6z%Wbv39h!U=Nw}(fXO;87LKmimiBxQ2brvm(mer{-Hm2RsQYSamS{9q5?hc zlgFzelg@tB(6INR1XN~-X6DK6i~u)c*xqW^GkmiStT)*|6Bf_F6cU-PYEb5dBgmz` zKFuJOZ`xMTTd6YS)NYu587%r3qqE+z(Bi5F6!0xFXWjX+!rsuF-7MC+ZsU?tqpe9k zAByh3@`~CNH`LYE#hk6Pb~*p#70fyKP!35?oZz(j%b{KOOZA(oGoO3SGmd$iB}~@~ zhyOn2}#IHo*2p1Cbwokw8!S@~JFfCt%voQKe+eE^;W!-#r3bFtzk*+pm2jG+C|_^`#uwHaG^(=UCCE z^Fvb0I~k2-gi~~Ck0zV&?^fx*O6U(sMHLMDuY6mZv37s+=$$w&$ebCo2{WR-tv>nH zd>vulk|uLSWaXaE8&0e(x#wb8cmrF ziIuyNv)aYI#0!j5#f#u~EvFevvr4C`vR z#5^A{*C;maVXRX~=jhV8j%{svC#;;bIh96jOjE+edqC9Yi&>kag1&x)zy9L4haTI-?@N_Sni@~0}H+iU2q=n=Cb^m3Xe z%lp;bdzk}WbGs#A7+-v5CzEYy*bt3kp+aou*=C$#moE0{uMyDs=$Nl@R$--CB_Z{Yo9M{4n|oR(IR^)bqBcwgU^cQ^X!tM$Vq& z7?xirA4G&SInhw)jGQtN#if@UMt$FhEU?KYCG2kUiwbftQR=~+FfVeV|# zafN&30f}0vuTqT?BeyZ?L$-K@EnF08av@h}o@g!DQsp`bIdFteIC13A!efKm2UHQj zcvMw6pyN*pA2Yk#wb<2N3gk&mhU}FVOrtCbP1*joxvl*#iH5l%lTCh^;N^06n71FZ z$>egl%+!O~{+y`+(cYq00iV1oo1J?Pf|*?^e{D)xH+=-(O;)^w@Po(%(ryf9I_flu zFU;YqQtmWn;3`R1P^sY6S52Xpf5%Szv!af=gm^-KC)xYKR2FsS&sEFuR`fg`Pkg(1 zx%GDGrIziyFC^o%9kW(Kh|O)97SYOKin^D>$Eog{u-N3rrR>mCSBr$zwBd@+OwY7hDb#~XfALESSgkoBjH)W5H+?lVg0T5tqs^<>{@z3#kTQ?=~- zcMCSyp730R>XR*My!_p>l}9|d0>ath@R6kH8f7}~ynPlPKO1WGlOp`whD!3~)=|ez zj&B@h`kh1^QTl>nY9prA#vc5N?6T;+DI487%+C2A)FD?=crsACONW~)g& z(1RJb1F|Vk_YZsP-=}5t8oSn6?uW^1cu)5IqtC@?t#bg*c>d0whXY0VLaa#tVMbJYWpiayE^B0rSnEJ}tMf@F9k$_Gc!6p-u^b>u+SF!F z^R`ij-WaAph$!ZiM1Q8Tk{4(Xd)D2UE%~0R*t2jqVXMd~r${??@b}MN2VRhdpmNkv z8=`x|jM<})qm}9AX`ZmDn_)JQ?yb5GgbOJu) zAAF@PhPLZ$u?wx$pNl`7Nv9hyExtcMUV%B8n7hI}m$gTKXq0}|?|BBisYp)haGdWm zZJ_^uWNpNXh*OG8l;(fJp#3{F^M4U({C9a^;onlH8J}YCTH2{pq5Pt-aY5SWSM;5= zf8H^g|1)Vo7t)K0i+h*zw60u-$6VwPh-Kl0hX5R(1sh5sCy>$dSr)*8eE^bHy@-g; zyu1Ei;CBJ32U0KhAdy#H0v5R&caclpfR6e0QOGXMzQWur`5&(DLh%KRR-R(u_kDoQ zA}IgED4;njoplJUDKSWNcnma9aGH&}7UGB*f+Cbkv*ofK2rtB}B6a+GVLfB@EiP950EiM>n^$43s=b zbpa?<-}3Bv7=20^-oGpV#8>;M~vOsS=%(&e!59AYzz&jAi!^>BSy?+lv9QOMi74C;(Z;Rb`5X3u0R%Dt- zlQ$vvYDI&1z8Ro5BeC$TU0=tPLwm>lw?QP&+3mh}s9Lea;78G zF*$sH(i+d+^XJWauJp6V#E*4Rf3j*&N+I6Ga{~LjpCT%@the%heScfksbxZ&8&K92 zuyIAi9nCIdXe`>+wH(;;{MB6Jk4OopcJq#4IYUtRvla1xp%MC2Xzi=1&a^%H7g2>k zb@bH)OVsbPTqOVxwzNAW8LIt4-&C?i_0m-mlGxmzAO^-K2pIQR95$P8k$dvEmtG0BZTKb1A-Cpg<3D+8z%y>lHM3fXog3QVoxB+^3P!qKz&59N%1>Ck zd&T&5Rq=QPs^%w$x$o(@Mk%&DQA*$@9IF4yiQ72RHnRN-jPhjMXL&0cnj_Y`Jlv)f z-7q1Q`2>vp5s=%9kH<5;?&npTL%j5_Tns*Tn1j`EK!DjdT1+!V^*aY^>$m8 zPnn%!EL5URq^<^-W%g1kZLo2$^!L-P*hY?GWwFduzvD0>?d%_wFhCJS=A& zWYd(RrSRNP+7v$m9{gR{A8sAP6t6oVEu-U-Ew9k8DkI;4=SiIqVj^I4rVlfUyOc)D z@b@vY-ttK*?g~anU|K&rl#p! zX(H)(yd9?x^h^#1n8%^%W^2E?oSG{3EY^9U(8lT8xglzNVtd?tB*cJGAgBG`2?}_l zc1p(PcuvF^W@k-iFE?Oi-c?lClgHe%G|DZv+jgPLqipVZYiKa6{g?3_;U|8}9Xzgy zKAPaSqE(@1n`;F9KMu~n5$Oe-#fKvR;bW2=bS!^+q&E2c8b+u8)c%mpY}U@H`)da_ zU7CpX`(iFOnpk;Br?B14&bIXrHzv=8)^tA360i*mGJd5lp#!Zga16;-fhypO@c=Fg@hVAtG-vna(y_FYu+Wd)kmc8AV*_ zAf?cQ-41V#y&AXP3$ItO{$uSGTD7t7w38Vv?={Ux*F6|z%u^nEJcP=->2znvUUFz7 zZb&};-2S?MYn#ua;~NWY;sgTa)g(LD~u$`UOfVSTJYeKRvAPwWwYG9tk8fxQ=MO-qa)IQ2p7>GHf zUs8p-*E^54=IQMREFN0B6u+Jr`I>WbFl@LQg8hRL@qC1+K3ztP$z&*+n58+t3BE9Coy2pfTnz8o3o=oCVGQ7^C}u4{UPk_ z+a`?uN(a*51+;0L^4?hnIkFETPJgERriP`KhDml>qS;s>Ap{llAbyf%-Iw86>U0J7xqbcX5L3ryrNnLp1KZ z7?cL!424tL=HNnEqg+TN&oX49ye9UTjg{lv?w8rva&EQg{5fXQRB z@^LNuM;Bua`WO03v?9kZL!;SDvYV~+dlnBVpKUKVTm*{h=f+-DvX_7?)R-&}^VIvEZ#_{sRU~XxGigm7nbg-N3y599xoEM>SQn!n(jB};H6`QTgzEDc$|Z@bye{@>~vQ4`>lZQ7FS*mD#%^hHdd2t z9cTq_O2{k}V>*782mR~8%R5)3Epd*WV|4l3PHvs{B%_&pW*ww|S#5HJUxb})&~(AP zYcmXeD_aO@F$HP|9J^j& zBVH_Xd-c`imO41&G0!RPUB^lnft zzePGC)m7t|sWk+7kn7ZL1bU$a$gK>rq&^21?XEiEYf?HK{Q4Pvp=%gG4=eefnxhE= z8jVZGv}AOdai;|6ShGR>nNil67ofveWv@jFp{oSKtu^`)26ND*$Aqz>U4WAr09gCg z!@@=fv7~~MxVj$YfTg#`TkP)uDH;uNEeL> None: +def import_from_ankiapp(db_path: Path) -> None: mw.progress.start( label="Extracting collection from AnkiApp database...", immediate=True, @@ -27,7 +28,7 @@ def import_from_ankiapp(filename: str) -> None: mw.progress.set_title(consts.name) def start_importing() -> Optional[tuple[int, set[str]]]: - importer = AnkiAppImporter(mw, filename) + importer = AnkiAppImporter(mw, db_path) return importer.import_to_anki(), importer.warnings def on_done(fut: Future) -> None: @@ -66,28 +67,17 @@ def on_done(fut: Future) -> None: mw.taskman.run_in_background(start_importing, on_done) -def on_mw_init() -> None: - action = QAction(mw) - action.setText("Import From AnkiApp") - mw.form.menuTools.addAction(action) +action = QAction(mw) +action.setText("Import From AnkiApp") +mw.form.menuTools.addAction(action) - def on_triggered() -> None: - file = getFile( - mw, - "AnkiApp database file to import", - key="AnkiAppImporter", - cb=None, - filter="*", - ) - if not file: - return - assert isinstance(file, str) - import_from_ankiapp(file) - qconnect( - action.triggered, - on_triggered, - ) +def on_action() -> None: + dialog = Dialog(mw, on_done=import_from_ankiapp) + dialog.open() -main_window_did_init.append(on_mw_init) +qconnect( + action.triggered, + on_action, +) diff --git a/src/ankiapp_importer.py b/src/ankiapp_importer.py index be14511..6c5ad73 100644 --- a/src/ankiapp_importer.py +++ b/src/ankiapp_importer.py @@ -8,6 +8,7 @@ import urllib from collections.abc import Iterable, Iterator, MutableSet from mimetypes import guess_extension +from pathlib import Path from re import Match from textwrap import dedent from typing import Any, Dict, List, Optional, cast @@ -236,7 +237,7 @@ class AnkiAppImporterCanceledException(AnkiAppImporterException): # pylint: disable=too-few-public-methods,too-many-instance-attributes class AnkiAppImporter: - def __init__(self, mw: AnkiQt, filename: str): + def __init__(self, mw: AnkiQt, db_path: Path): self.mw = mw self.BLOB_REF_PATTERNS = ( # Use Anki's HTML media patterns too for completeness @@ -254,7 +255,7 @@ def __init__(self, mw: AnkiQt, filename: str): ), ) self.config = mw.addonManager.getConfig(__name__) - self.con = sqlite3.connect(filename) + self.con = sqlite3.connect(db_path) self._extract_notetypes() self._extract_decks() self._extract_media() diff --git a/src/appdata.py b/src/appdata.py new file mode 100644 index 0000000..0a57888 --- /dev/null +++ b/src/appdata.py @@ -0,0 +1,57 @@ +import functools +import os +import sqlite3 +from pathlib import Path +from typing import List, Optional, Union + +try: + from anki.utils import is_mac, is_win +except ImportError: + from anki.utils import isMac as is_mac # type: ignore + from anki.utils import isWin as is_win # type: ignore + + +def get_ankiapp_data_folder() -> Optional[str]: + path = None + if is_win: + from aqt.winpaths import get_appdata + + path = os.path.join(get_appdata(), "AnkiApp") + elif is_mac: + path = os.path.expanduser("~/Library/Application Support/AnkiApp") + if not os.path.exists(path): + # App store verison + path = os.path.expanduser( + "~/Library/Containers/com.ankiapp.client/Data/Documents/ankiapp" + ) + + if path is not None and os.path.exists(path): + return path + return None + + +# pylint: disable=too-few-public-methods +class AnkiAppData: + def __init__(self, path: Union[Path, str]): + self.path = Path(path) + + @functools.cached_property + def sqlite_dbs(self) -> List[Path]: + databases_path = self.path / "databases" + databases_db_path = databases_path / "Databases.db" + if not databases_db_path.exists(): + return [] + with sqlite3.connect(databases_db_path) as conn: + db_paths = [] + for row in conn.execute("select origin from Databases"): + # Use the first file found in the database subfolder + # TODO: inevstigate whether this can cause problems + db_path = next((databases_path / str(row[0])).iterdir(), None) + if db_path: + db_paths.append(db_path) + return db_paths + + +if __name__ == "__main__": + appdata = AnkiAppData(get_ankiapp_data_folder()) + print(appdata.sqlite_dbs) diff --git a/src/gui/__init__.py b/src/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/gui/dialog.py b/src/gui/dialog.py new file mode 100644 index 0000000..b22b581 --- /dev/null +++ b/src/gui/dialog.py @@ -0,0 +1,86 @@ +from pathlib import Path +from typing import Optional + +import ankiutils.gui.dialog +from aqt.qt import * +from aqt.utils import getFile, showWarning + +from ..appdata import AnkiAppData, get_ankiapp_data_folder +from ..config import config +from ..consts import consts +from ..forms.dialog import Ui_Dialog + + +class Dialog(ankiutils.gui.dialog.Dialog): + def __init__( + self, + parent: Optional[QWidget] = None, + on_done: Callable[[Path], None] = None, + ) -> None: + super().__init__(__name__, parent) + self._on_done = on_done + + def setup_ui(self) -> None: + self.form = Ui_Dialog() + self.form.setupUi(self) + super().setup_ui() + self.setWindowTitle(consts.name) + + qconnect(self.form.data_folder_checkbox.toggled, self.on_data_folder_toggled) + qconnect( + self.form.database_file_checkbox.toggled, self.on_database_file_toggled + ) + self.form.remote_media.setChecked(config["remote_media"]) + qconnect(self.form.remote_media.toggled, self.on_remote_media_toggled) + ankiapp_data_folder = get_ankiapp_data_folder() + if ankiapp_data_folder: + self.form.data_folder.setText(ankiapp_data_folder) + qconnect(self.form.choose_data_folder.clicked, self.on_choose_data_folder) + qconnect(self.form.choose_database_file.clicked, self.on_choose_database_file) + qconnect(self.form.import_button.clicked, self.on_import) + + def on_data_folder_toggled(self, checked: bool) -> None: + if checked: + self.form.data_folder.setEnabled(True) + self.form.choose_data_folder.setEnabled(True) + self.form.database_file.setEnabled(False) + self.form.choose_database_file.setEnabled(False) + + def on_database_file_toggled(self, checked: bool) -> None: + if checked: + self.form.database_file.setEnabled(True) + self.form.choose_database_file.setEnabled(True) + self.form.data_folder.setEnabled(False) + self.form.choose_data_folder.setEnabled(False) + + def on_remote_media_toggled(self, checked: bool) -> None: + config["remote_media"] = checked + + def on_choose_data_folder(self) -> None: + folder = QFileDialog.getExistingDirectory(self, "Choose AnkiApp data folder") + if folder: + self.form.data_folder.setText(folder) + + def on_choose_database_file(self) -> None: + file = getFile(self, consts.name, cb=None, filter="*") + assert isinstance(file, str) + if file: + file = os.path.normpath(file) + self.form.database_file.setText(file) + + def on_import(self) -> None: + db_path: Optional[Path] = None + if self.form.database_file_checkbox.isChecked(): + db_path = Path(self.form.database_file.text()) + else: + appdata = AnkiAppData(self.form.data_folder.text()) + if appdata.sqlite_dbs: + db_path = appdata.sqlite_dbs[0] + if not db_path or not db_path.exists(): + showWarning( + "Path is empty or doesn't exist", parent=self, title=consts.name + ) + return + + self.accept() + self._on_done(db_path)