From a385308f38b09664b7ddb6ce3cbc8e17de614447 Mon Sep 17 00:00:00 2001 From: Vic Sergeev Date: Sat, 4 Jul 2026 16:04:48 +0300 Subject: [PATCH 1/4] working --- .gitignore | 0 .idea/.gitignore | 10 + .idea/CapLog.iml | 10 + .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 7 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + app/__init__.py | 3 + app/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 237 bytes app/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 241 bytes app/__pycache__/auth.cpython-312.pyc | Bin 0 -> 2748 bytes app/__pycache__/auth.cpython-314.pyc | Bin 0 -> 3047 bytes app/__pycache__/config.cpython-312.pyc | Bin 0 -> 1897 bytes app/__pycache__/config.cpython-314.pyc | Bin 0 -> 1982 bytes app/__pycache__/database.cpython-312.pyc | Bin 0 -> 13725 bytes app/__pycache__/database.cpython-314.pyc | Bin 0 -> 14356 bytes app/auth.py | 39 +++ app/config.py | 51 +++ app/database.py | 245 +++++++++++++++ app/models.py | 0 app/ui/__init__.py | 1 + app/ui/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 178 bytes app/ui/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 182 bytes .../calendar_widget.cpython-312.pyc | Bin 0 -> 9779 bytes .../calendar_widget.cpython-314.pyc | Bin 0 -> 10341 bytes app/ui/__pycache__/dialogs.cpython-312.pyc | Bin 0 -> 9968 bytes app/ui/__pycache__/dialogs.cpython-314.pyc | Bin 0 -> 10531 bytes .../__pycache__/main_window.cpython-312.pyc | Bin 0 -> 12006 bytes .../__pycache__/main_window.cpython-314.pyc | Bin 0 -> 12349 bytes .../__pycache__/note_editor.cpython-312.pyc | Bin 0 -> 13373 bytes .../__pycache__/note_editor.cpython-314.pyc | Bin 0 -> 13952 bytes app/ui/__pycache__/notes_list.cpython-312.pyc | Bin 0 -> 11480 bytes app/ui/__pycache__/notes_list.cpython-314.pyc | Bin 0 -> 11876 bytes app/ui/__pycache__/sidebar.cpython-312.pyc | Bin 0 -> 3464 bytes app/ui/__pycache__/sidebar.cpython-314.pyc | Bin 0 -> 3643 bytes app/ui/calendar_widget.py | 189 +++++++++++ app/ui/dialogs.py | 219 +++++++++++++ app/ui/main_window.py | 212 +++++++++++++ app/ui/note_editor.py | 297 ++++++++++++++++++ app/ui/notes_list.py | 230 ++++++++++++++ app/ui/sidebar.py | 59 ++++ captains_log.db | Bin 0 -> 20480 bytes main.py | 20 ++ requirements.txt | 2 + 44 files changed, 1614 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/CapLog.iml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 app/__init__.py create mode 100644 app/__pycache__/__init__.cpython-312.pyc create mode 100644 app/__pycache__/__init__.cpython-314.pyc create mode 100644 app/__pycache__/auth.cpython-312.pyc create mode 100644 app/__pycache__/auth.cpython-314.pyc create mode 100644 app/__pycache__/config.cpython-312.pyc create mode 100644 app/__pycache__/config.cpython-314.pyc create mode 100644 app/__pycache__/database.cpython-312.pyc create mode 100644 app/__pycache__/database.cpython-314.pyc create mode 100644 app/auth.py create mode 100644 app/config.py create mode 100644 app/database.py create mode 100644 app/models.py create mode 100644 app/ui/__init__.py create mode 100644 app/ui/__pycache__/__init__.cpython-312.pyc create mode 100644 app/ui/__pycache__/__init__.cpython-314.pyc create mode 100644 app/ui/__pycache__/calendar_widget.cpython-312.pyc create mode 100644 app/ui/__pycache__/calendar_widget.cpython-314.pyc create mode 100644 app/ui/__pycache__/dialogs.cpython-312.pyc create mode 100644 app/ui/__pycache__/dialogs.cpython-314.pyc create mode 100644 app/ui/__pycache__/main_window.cpython-312.pyc create mode 100644 app/ui/__pycache__/main_window.cpython-314.pyc create mode 100644 app/ui/__pycache__/note_editor.cpython-312.pyc create mode 100644 app/ui/__pycache__/note_editor.cpython-314.pyc create mode 100644 app/ui/__pycache__/notes_list.cpython-312.pyc create mode 100644 app/ui/__pycache__/notes_list.cpython-314.pyc create mode 100644 app/ui/__pycache__/sidebar.cpython-312.pyc create mode 100644 app/ui/__pycache__/sidebar.cpython-314.pyc create mode 100644 app/ui/calendar_widget.py create mode 100644 app/ui/dialogs.py create mode 100644 app/ui/main_window.py create mode 100644 app/ui/note_editor.py create mode 100644 app/ui/notes_list.py create mode 100644 app/ui/sidebar.py create mode 100644 captains_log.db create mode 100644 main.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..ab1f416 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/CapLog.iml b/.idea/CapLog.iml new file mode 100644 index 0000000..f2cca7e --- /dev/null +++ b/.idea/CapLog.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..25d64c7 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..2089c61 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..b71b4f8 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,3 @@ +""" +Captain's Log - Приложение для быстрых заметок +""" \ No newline at end of file diff --git a/app/__pycache__/__init__.cpython-312.pyc b/app/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..70737b62c2349276c19a50168e4d0e9449f442da GIT binary patch literal 237 zcmX@j%ge<81l7k?vx0&2V-N=h7@>^M96-i&h7^VLfPrX>d zCqG?5SK-3^iwzfcT-beK--T@#wqDo^WN%ftu;s$;i~R~0HeT$$*m$w&VgrzFRk*PI z!iEcbE^NKn1XR3>%TJT>7JGbrN`7*D{7Qz;AWMJgx>&`yxQ4|9R61uQ7Uc#M2KczG$)vkyQs2k+&Vvx5!Ff%eTeqv%` JWGP|+aseZzQ@H>D literal 0 HcmV?d00001 diff --git a/app/__pycache__/__init__.cpython-314.pyc b/app/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dff823cff660ce38cac6b77f96ff297d27b5df02 GIT binary patch literal 241 zcmdPq}zQ`1q9!pF!5%(s!|nad8cc38-|=NG!??D9X=D zO)e>p0ohs%v@j;IpdcnbJ~J<~BtBlRpz;=nO>TZlX-=wL5gSlH$nC`-cYk1JWMurz M#Kg!_!~*0306tPvvj6}9 literal 0 HcmV?d00001 diff --git a/app/__pycache__/auth.cpython-312.pyc b/app/__pycache__/auth.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ae177e6a63a627cd419e8ef70aa1a3a2409b6c24 GIT binary patch literal 2748 zcmcImU1%It6ux)%Z}PL5+Qc+Av2Lpc$JX@E){>^NZ7N!^OF{8r2@I2+X*TI58}Hq< zZdgm&MoB>lzL*L^6%ktcvIGm!TI-tyU+%D$q*H`~zW7#>2cb`%b7%I~RIT8JJ@=kF z_nbNVo$sDAKSiU>1jel&w~nb{LViW1Jc9mWbw4cTiA55`qLz}T2`XnLp}-tS2gZYm zASDWUg;>EEVuh|Mo@FAGSM+dB2Tgn?z87CR=f!pB!UV*nM= z6*tK3^hv33G>gR^=tuj8j=ucX(24wD%4FjwSZ2&lX8F*dIr(~KWXPPH95Ugpy_0!Y zHH^`T(X3%iMKO@xvZ{h7-UMQrEU)jF4Sp0^1%=cFiK&r6egX>!l8jM2Q$Gsys=xc5 z0w-$!qGx{k*sd$}~I$YPcQaCePZ;m^3**one-dGI`2vt*G42x^!1u54dgeQRSe_g4+Vm z4fth8Cr0!DLnvLfZ0ttMD{0CRz*sEC5No#`EgN1w)g=?N#*-E>;RirWlY3n~OI_QX zu5FiJEp_dkjV`OLbCC;?CAHg8yG76L;_;u=BXye>t`G2d(Cw#tZta_U(^-XMA)w;h~`OELosn2>+U5sQMsn}9A}0=H$P zia1*bJ3j0TuMgxSn_;$>#d^Hkk|{fRs*=8`b_sR;=40T3?*RcQcW#^?`FLchQ+GOb z@!X+O=i%9wJKDzO_6_&6_9bnLqiy+oe@WY3Y;m*$LOtN2TYD=icjgdyd^B{e%8jrB zK5oHWAGoQ-&6xxA&TWrDZL4Mcb*ytSaa}CNYMIX3z}%%fnFjL}MS{sXaZM#{Ifm|Y zQyb+Kppe~D9TKdD-6z0+Zvp{mY3*~TFP!#a87S^4X)iABakK%U4*XxhR0)OcLVO`^ z0+ou+EEM$xnB}!^1OX61zQx3X^FHao9G$qH%WVEt&MDNq)v5{HsJbf$&^ ziB8ulkU*ld{|!G0hKJQga30rvX5m$WwJeCCcg%Fo_B)nW{kpNnJp{a|SfvU+_1pW6UF5gbI%5`0<=@jT%CG9F$;wZPev52SF%JLZ~cQl8= zim%|R0yF)h=d-h>59vWSf}JNlI_!pd%G|wopBuI(k{Qc(n^N|>*64`Mvp%4*)YZZ% z>!4rJ>buldcsa)irpa$w=iHeKXD+5LWj@W6Hti^BJD0RRN9!wT{X*@RV8!(o{7jfI z$8E!Kn+;<;W98DgZZ(Xzb7tCm!muSU{E%ZED7sJ}=RG!T!4;O~D@1ygDQDOoq(Hzp zw(1#jU!kGLFYaF@Fs+1xp(z_IrP8fbD<>$N`S`{CQIu literal 0 HcmV?d00001 diff --git a/app/__pycache__/auth.cpython-314.pyc b/app/__pycache__/auth.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eb6fa0fd3453b933867b6df277e93a7aaa1015f7 GIT binary patch literal 3047 zcmcImU2GIp6uvY2H~pPLf29?+AT>*|{Ipzje;pH=Iv`_Ple>Ics0HZ-QKO&fAOjcQE!L z)QjP35?78I%$PDPGwRwPDtB8_h(Ol>linsw0<+}F0Q0nw42h6_Nt4=1O3`GHP0Wi1 zff-(qe$59mp!q>6S^#8?rhu%~YFJ%_uzIZ)jIHC|Ls^!HN^k^zSnymAe1#y0VJQda zOv;1j35mgI7kN!ez^SiGZ;@U2yYe5np3AW*ij9!W{nHsbV=&&J>(P{9S+Qi4>AHop zTXVek=+MN`mm?G7r;a2HIx|i)C(J0bCXN`h%z8BwpD<=;Ck(KuZ}t?g*L5SE&M-c(=ArX1-7(v>4vO7W9 zz9RgwfbRJxkI`08cmuFT+VZ;iD*m(sMqDS*O=8w!8EO`NT8(~VG+{>HDLa!l3*f5< z(V1312*A8;)93Ne;!ABIr!8df7|pjGx={D4y6IkX`vbLkN!{wGTfZF4t2^iG9QA;$ z94KO4c}vQ7XjJU@G+1lp8=`q!+`L%~^_93ebAaC2ok0lKmHfYfp*v{buor`syr*rT z&ZXV79qMbc0F!RPwj@Ywq3LeiP?J^}DC9bC5n!zueFhY)%^(0RwR!I3#gi^9BlG=v z^~J@0M;)=1k^c*rRYDDd4fa>|Euhl8a{=C|1*k<3SPKFmf_w|w3(g1lXBcHIM8#XU7u{t*HyJgN>ca-C|n?d{y+S9N@j!b|omh~Q` zZW58!3vvUlPU$fm$Cq_YDXqbGJB#Mp-@xSsuDW5+M)av?7CYDWM8*-j^vqs9DQFVPfNiCL2 zrpmkqUi@A9(#ph8RdyN!zi^K_PbkjyU{elo?V2! z_uWpk=qbBXzO4c9tk13QttSV&)?@4IgO3k(tOp05SdR~G_E|rG^Vs?hmcO;Wv7RvT zca}b{I7J-G*PRzt7wb0Hd+!s+RNYH#US>Hk$>4;Z(|LINsvTdJ)LIEi7l8H zqu4LDVnK}IfY_!Y9K@l|TsVv)V4H9=*cKcG+lpgg+iblZ$ALR=C)h503~V<(4z>sP zg6+d6z$Wa~etZ)6l$~|ju9md96Q98moWgJ5vtRhWbcyZdbEyH!O=_jQTG$Rx%A2OF zmXb!_tXA0er3cRsrYV=#wQW+mwB4jQ%Z60c3WGSO?!seNKZSdzz7LO0?PKn{;8J(4 zu+8@m=h&p~Jah8)paDb0Bq!^V9Wi1LDiExi+L5M=z@$QM%QUr;6E`Ze`AW+dXsEut zrhQ0sC_g5%NjX_jNXY~toxT7}LyjOF#ii3@`Ef!yESCy)A|4+b8O_s>BfN@J#z)e_ z3gwM0C}QMODuS0t2YnVIYo<>j1Vhe7AOYIox^7oGCnbxkc%<}OZLnW3ST?0jZn zMOw*bzzf&CnW^mb%oQ45&Ss`APAt_|o9D9gfDQ324N5(Sn_QS(SX#b^Ua)W3%d?rc zRx?vqR~O5ZYqGwvsB7zRM8+yyX}HU)^5*8MV&C$?&9csZSZ%`r7-hJ;yQtdKvolkT zcJ5sMgG2tN>qcef4*7*YS&hbSCvPQh58WEN+rK}!H+UF5e}jM4peFW{d&&Kwy`cw* zN6ClDM?(*XemrwHeEBdjdl;R&&eu59)VDKTjWpjZ+$h{E-YDL=aCiDJa&BiDmSXXp z*(#Vh$HcoVZ>-vJ%<{I?xOjxC`a{>%ng{5aO+FyV651+ijc4pfLag#$O<){wILJ8U zaF}rf3Gq8Zt%-3n5?U&$S_|W-qqj1SIeHu8c1Mph?m$AcGG6Os+=YbJO15^4akrx% zXWZj(FXKKWv{%Zt6O0oM_cK0;x=&SGyVi_@% literal 0 HcmV?d00001 diff --git a/app/__pycache__/config.cpython-314.pyc b/app/__pycache__/config.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b4e1387388663b8059a65a4209248fb5efd66500 GIT binary patch literal 1982 zcmb7_%Wo4$9LL9x?0OwP63347;E@+KDa1B;6jD`{LL3r{JgfncBE_uMyK85W@us_z zisWRe2thBH6H!laLfoKL3sObfTdz)Ys5@Mc`VSaIYOnpx?mDviklGpPH=p14_nVnt zc4nM2p`af@`|77tH=lJN^c!JW)+I0GjUaR z_Opgg$2t7n#Wv3);(`L)H#oKd@nSdW$aBLe&tnhvPJ8U^gfmG2IEM1RT*MLDhFq-8 zaWK#4djGCY!_KUcz{<-1SnI#<`p=nz_O9(VA8OU|*O3Hp2MPa6b;Ipz*qjgK!$(Ke zjX4s*Jn6wc62$_EVL$0jA>4@r1s4wD5ZEy80^5xvV0&;BY|PSoaU8f0_k$h4gJ6&0 z<6uwVlVFGNFxU|*I*P}DPg%3xvsO!3d>Z%Tar{0$gG2Z%p1?_*N_EV;NF1k0A3g_{ zXp(aCdZnn9c7yZM7Ll|{!W>%GOS>KE;tMq-_Ek}Rtg0zx`{9+(DfL2Tuv(=)|cm2!$yDOnSh z*_o;7A`RHWQJy?Im7bI-Z*Iearpb1Y3m0Z&>La=$?Lb+SC+clt*Ec!Cx}!jKow8I` zD~k1JwmA@iZ|FoeRY_3|%9k~>wTdzc-BBPnXoXC*tja`HpoHl(>r^n+EyaUCpemi|g48ctOWoTF5RgU8TV*+04SlIqXEc zRE`vF!3n$F!gx!Y4S<(Aad?L|F%sXd75~Vc$#{WdOq?p@gngu z^&<7->4VA32P4Y|k(GOVlSAR5>S80*^`LaW^q_pdym$Wb;z8(abrC|*cy+k}X2mw~ zfx0)^=o_qiV@)m|;u^ldJ+0{hdTUV!5X=%{HLb}r_8}o!yWSKS`)%%I9I!daID~}w zp3n?4?m|L$ZM@mdIAZHPjH9+5W87=&amIZ}h}34A{fq~Y&{NAc2N@r;_2Z0B*nE=l z5E6Q8JI!InBQ}pR9z(*w-fY9yU2B>6UVp>aQ@j3d0^)Zr88fYe8TV3N6ct^DV;*DT yXe(l!x}Ep|Xw>(S{VYBH8{|7hW;tmlQYv!L2j11%e literal 0 HcmV?d00001 diff --git a/app/__pycache__/database.cpython-312.pyc b/app/__pycache__/database.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..02bb23479108af6a910948e657b73ccf791c49d0 GIT binary patch literal 13725 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 literal 0 HcmV?d00001 diff --git a/app/__pycache__/database.cpython-314.pyc b/app/__pycache__/database.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b3d88c34f4a6b52af40e47e93dbd05d9e0df6b21 GIT binary patch literal 14356 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 E09%$ZivR!s literal 0 HcmV?d00001 diff --git a/app/ui/__pycache__/__init__.cpython-314.pyc b/app/ui/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9f8842e9050ed5fdb6abe10fa33d8a7f60cb9544 GIT binary patch literal 182 zcmdPq^MASDe9K@24fL5#`_noOnqK!H$Cg$sKw?7Oh# zV)KRF7k2n*GTvg3k59=@j*nl-@EN4_mXV89jEienOhBb`Mq*KJKv8~HYH~?&jB{c^ zNwH6UdQ4(LK}>08OniK1US>&ryk0@&Ee@O9{FKt1RJ$TJpb;P^6mtNH56p~=jJFv? Ii&%gh09jEng#Z8m literal 0 HcmV?d00001 diff --git a/app/ui/__pycache__/calendar_widget.cpython-312.pyc b/app/ui/__pycache__/calendar_widget.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..647fc2330467a1992b595a9e455c772e2784a6f6 GIT binary patch literal 9779 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 INE5C82akcaJpcdz literal 0 HcmV?d00001 diff --git a/app/ui/__pycache__/calendar_widget.cpython-314.pyc b/app/ui/__pycache__/calendar_widget.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4727a79be251e41869bcb580ae7243bda74e36a9 GIT binary patch literal 10341 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 literal 0 HcmV?d00001 diff --git a/app/ui/__pycache__/dialogs.cpython-312.pyc b/app/ui/__pycache__/dialogs.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4ef234e1a01980f2d047f3d767b95f88280424e0 GIT binary patch literal 9968 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 TvW?59R?b~N_XPt@iuHd0nbs>Q literal 0 HcmV?d00001 diff --git a/app/ui/__pycache__/dialogs.cpython-314.pyc b/app/ui/__pycache__/dialogs.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..643cfa6b0a32651144d12b8c8463e8c0751c1ea2 GIT binary patch literal 10531 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 literal 0 HcmV?d00001 diff --git a/app/ui/__pycache__/main_window.cpython-312.pyc b/app/ui/__pycache__/main_window.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7db96acea1b62f53ebaa7a97849c8f3e446ebfc6 GIT binary patch literal 12006 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 Z<%j=*wbf)OJ+)%Ic&zwq3d5v{{{z^le^LMd literal 0 HcmV?d00001 diff --git a/app/ui/__pycache__/main_window.cpython-314.pyc b/app/ui/__pycache__/main_window.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..81a947d617eed85e4004fe5701d473c6f8478e76 GIT binary patch literal 12349 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 literal 0 HcmV?d00001 diff --git a/app/ui/__pycache__/note_editor.cpython-312.pyc b/app/ui/__pycache__/note_editor.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5fae9bd139f2bddcde1b95ddab98dca73f0c3e6b GIT binary patch literal 13373 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 Kj>0mT<$nWB@wUAH literal 0 HcmV?d00001 diff --git a/app/ui/__pycache__/notes_list.cpython-314.pyc b/app/ui/__pycache__/notes_list.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9ea1fbe2643f89095a7c143b394a22d9b4b5f52a GIT binary patch literal 11876 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 IY0}mI0$6-T^Z)<= literal 0 HcmV?d00001 diff --git a/app/ui/__pycache__/sidebar.cpython-312.pyc b/app/ui/__pycache__/sidebar.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..be120585f6159788b71e3edddeda1ee489783724 GIT binary patch literal 3464 zcma)9U2Gf25#BrAkxPoCY*$uf)3%h_R!Y}Zl(b0N{M1hL=OhjtL`DNJLJt)0B$1{_ zcJJs~0u88+fsl<8CJ%uP6lfG6K%H7Z<-kT1J5Kx1hXQ@MD`Zf))-=E?2i5(iG&E0y?;BHJwgfj4>r6ayOp(HLggY+Ns_37D&~Zw z2wz{$H!US40d-PNp7tmGye{Vg(=WG5>5XnzzjB=1uFuT{KR_1=~MR$WLS@p%xuT<+Qw-Vy|V?NzDLF zFi|ix{dh(<($3tDbQdCP&qMo7a780Y0o(yMl0KDI$OO1`KA4nL-+7XhRSBeDl|cs3 zfh1J}AcGZhMo5N6Njzu=Ml-55o?@k5n9=+lY|-8E)h!)J`&+hz(BiJvRzbWkTqH)L zv+6ICx1ql*7?P*VwvKg`s;~=cEjDm`8_R;|fh>@+SSE^hq@*lCt1)YtWZO56B}DaI z5gaXyCfV(qSV*e8;J+faSbgt7tFo^wH*ID}Jdm(Mx&xpxJ{cEnaeUI2f1Oc{sbO2n zX%mJm%z#=#BU_rtRDRLAXTA+O|t5Q?tlw6tMIi>dS}rN|1!)MUzliLq@*(@cYw0?Px5^mqW4)p8TI zznEfL-e4WL3Qnb>l`|FF=_&dAD$Eb_1r;ToOX<3z=mF^O;brcdM+cu88yxz@*ef#w zQzs(8FGIhHCG$4Z$o7mvQ;3`3x2x48`K^W1F%t&X(*dXD^yMydge zc3tZHX!s8=fB5p^Ve7z=zn-p-yl#!WZqlx0I<~T-qas}htn52nk(O!f-!xpOF^k47 z4K35}uh4KUSPi~=dVZi1T&6#0_9iYTZqujNU>TD3!Uh{i6;9Z9fD%Agml!}(q=riX zH1rB+8}#*bge(XiA%T94`N6h9#ingG*sOasRa|HxrGc36k_~&c=C@2#$%uOG>^85~ z{HijB5D?hi$ys07xB04SK&1;o&rTplgXZ7iKQd=SDT@oCD=sd8Kx6-aK<5TiXl}Zf zHZ{BuSr<-GxE2{+pSExYY8c6P@kH#Pt^tWtMu9;v`KPqZ7;eDEp)GL=Lqpiql^k5;af#s}asT*U*nVj0xG=^S+lQT9AZ9zg zh*=-d7^?b5Am+$_yLwl`+u*M@QXN@|b=PD4R;>SiK-v|8Sc$BLNH9{1R3mk|+oHSY zeJ0(#Ob^`mlbsJ;JoetPOD8Wse9ix9q-R;0oPdi&mqT2Ij{vpRPog z=@VccJFv+zE3x?fpd7=M+E>H4QhT+%PWM=J&wTG~>XyftN65ftFZrnSQA3}Y$%R7^e z?7&_f17wcC<>NrnZPD&|={7z19Vi@yAzPuqb^tn!iwO+fvK|SJ1^V!RVDTyV|G9bH zItNHBVU!`68ZL8^wz~W%^saMx9jmcdN4kQ`ZSD28egA0N2Rx~VyR9(#9X`0~BjIkx z`Bpa?6ez>sEuHTN=Ua&bwpg-_C*NtK+=4`#+`+9B;=sKG!x1>31ZHVVK$QzU)^8wF zTLKBmTaVcUjW@4B_Ezfw2(OzrtT(JGd_QZjECJzt7^dW;Vvj@3jyO-Ort$~(V}bKa zM?9Qhw<9j~_?@B8?(`_OPW=4I9yVLhxUzbd72Zcji_)Iu7vGR$81 zh0HL<8M2BuoZ$lt1r#}b#M~L zPE?dKecmhOCV>1&v$wmQ&3A}DRjd!D;){GtoCPtSzoE}Kp!}Q-LlarUH@@l<1mR2a i@IOh%9kTZhdF&2(7&-6`m!Rsw&M9(WBdT2|IpomeE})powgw8cm)u-SXxdxfTkevS ztfn1^vomkryqWptdvBhe4Tk~*%Ar3Uee*OWEsr3m!( zpX;CKcj5PuzHU_d^iTD5{l0!jzh|t*V}j`&FXZR53(yLWD>*fvQP^xYv!HU2_~Qkx zYG<<=Pd64J#9oDL{|KZCQG{i3k#GS{M%9 zTZp+#*W7~X`AIgz7pF`yr_OUzxCUYol}vFyo6E%kcaK&os?4OaoXuysEHg9;L^*f% zoOIp{ou8SQ{J|@dJUcUSVcK+M=FD&*FXw?OvZm(LG&jRVC4E^gvO-Z=P&in|z8Ow4 z6?!tP5A@Ptb0X-^o8F?r)I4W+Aj?z+wQ8nVha}Kj#b~*;h{|$ z`6~@pX~dwB8FdaVPQ)Q%lH}|5LAOrBhHk>&Xge(loOZNM$dnb%a4ZpYG#cN2 z*x4mlD(pxSi0GaoxtpCx5}n-@-AVWU>t6~;EkO6WJEiJm{{|$GDtk)OC3#0}ga8Y? zSE~RPT~534-FGiOiAw&4<4FN{X1wavqLRS8MVQ zHDl7W9kv-B$#n6{re}&NORD9z41))iU{e&)`Yy`!~=m9aW(WP zK>VRfs2r-&g9bgg=GN)KO**jSC7s>Boc_i1jSDyXZ+kxu-3o17{$k{Zn;oyN__t_# zC0q{Q7+;&ad1WQMNnZr@$iP0!Y(-)_eoq8fYJC*Mm0HWKRoZLN-nGH?4xRQspf9p0 zY&6~MP>3-6g4-vpBLG3*ySF54LN?2jLL+utudb^!2IKl~^jpR^bP%P zpZ<~Yt|fqhKK(Yl$JdQ_LD~oJ^SXWy9=Q9KbXmUx3}D{{JEo7R^Gwwip^q12ig^1P zZhdgDb}PqF+~eHJUVc0aqZ+!#GJ)_@10!nC=$iO|9{mO!@chddcHq0WJYx$oV7C`_ z>Vc}Exfd53n-S3XKd|{2@c5&C$G8s2Y(Sb~IZPb_i?+=W41?8O5bWYP;;8jQ1rW5f zR$C6kpWbp9$WskQjUYl1Jo*ne2}WypG@qu|G?vU8;@pY!8|O9dOlF=W4*t% z{lfHaj@ekg3MLRsU=3z>8^EU3KI5m5;cY+-NnfAdz|rd7hODpB2dLiB?;39#W%&M~ zPQ*CG0A!2BI|fG~W~lZGs~Icid>~j9)DWK<_so#ZPAjlzW~al#UE<@QuMI(E;a@pb zIkj>MqrijYJ%?G zvfoR31k!Mb?5+~-Q~xcXcMkvpx3xg6Ij-f;_x1ZQ@ScO(+vunkYTt*6>?Nq0@o9%l z$4+Hz5X}P)Mo$c(1q~w;;57XFhf5zW-CSDXmAA@oRa=i4t;cja`56STNede1vhZcB z-55G~%pH%%V`0qQ^6&)6vKf%&r9!5Z!*)=Xe_T>>_DHKN&u5v&bJ@I_FTk)*mNSJk zkVq^Jm{&{xLBYM~4h6owV1*BY+-?{@%=FU66sMd~^ExTC`}`gq9Y8GKCLPzBtc zSA?$vF3*(km|$%oV;P9GunCxM`uOE+o~w-Ey_x~Y0Eg4oKRmUhwhINN?6C8O+R3T) z;F|6BVB?jv9#ZxlFl8yH literal 0 HcmV?d00001 diff --git a/app/ui/calendar_widget.py b/app/ui/calendar_widget.py new file mode 100644 index 0000000..2eb5e2c --- /dev/null +++ b/app/ui/calendar_widget.py @@ -0,0 +1,189 @@ +"""Виджет календаря""" +import tkinter as tk +from tkinter import ttk +import calendar +from datetime import datetime +from app.config import Config + + +class CalendarWidget(tk.Frame): + """Кастомный виджет календаря""" + + def __init__(self, parent, db, on_date_select=None): + super().__init__(parent, bg=Config.COLORS['calendar_bg']) + self.db = db + self.on_date_select = on_date_select + + self.current_date = datetime.now() + self.selected_date = datetime.now() + + self.create_widgets() + self.update_calendar() + + def create_widgets(self): + # Заголовок с месяцем и годом + self.header_frame = tk.Frame(self, bg=Config.COLORS['calendar_bg']) + self.header_frame.pack(fill='x', pady=(10, 5)) + + self.prev_btn = tk.Button( + self.header_frame, + text="◀", + command=self.prev_month, + bg=Config.COLORS['calendar_bg'], + fg=Config.COLORS['fg'], + relief='flat', + font=('Arial', 12), + cursor='hand2' + ) + self.prev_btn.pack(side='left', padx=(20, 0)) + + self.month_label = tk.Label( + self.header_frame, + text="", + bg=Config.COLORS['calendar_bg'], + fg=Config.COLORS['calendar_header'], + font=('Arial', 14, 'bold') + ) + self.month_label.pack(side='left', expand=True) + + self.next_btn = tk.Button( + self.header_frame, + text="▶", + command=self.next_month, + bg=Config.COLORS['calendar_bg'], + fg=Config.COLORS['fg'], + relief='flat', + font=('Arial', 12), + cursor='hand2' + ) + self.next_btn.pack(side='right', padx=(0, 20)) + + # Дни недели + days_frame = tk.Frame(self, bg=Config.COLORS['calendar_bg']) + days_frame.pack(fill='x', padx=5, pady=5) + + days = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'] + for i, day in enumerate(days): + days_frame.grid_columnconfigure(i, weight=1) + tk.Label( + days_frame, + text=day, + bg=Config.COLORS['calendar_bg'], + fg=Config.COLORS['fg'], + font=('Arial', 9, 'bold') + ).grid(row=0, column=i) + + # Сетка календаря + self.calendar_frame = tk.Frame(self, bg=Config.COLORS['calendar_bg']) + self.calendar_frame.pack(fill='both', expand=True, padx=5, pady=5) + + self.day_buttons = [] + for row in range(6): + self.calendar_frame.grid_rowconfigure(row, weight=1) + for col in range(7): + self.calendar_frame.grid_columnconfigure(col, weight=1) + btn = tk.Label( + self.calendar_frame, + text="", + bg=Config.COLORS['calendar_bg'], + fg=Config.COLORS['fg'], + font=('Arial', 10), + cursor='hand2' + ) + btn.grid(row=row, column=col, sticky='nsew', padx=1, pady=1) + btn.bind('', lambda e, r=row, c=col: self.on_day_click(r, c)) + self.day_buttons.append(btn) + + def update_calendar(self): + year = self.current_date.year + month = self.current_date.month + + # Обновляем заголовок + months = ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', + 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'] + self.month_label.config(text=f"{months[month - 1]} {year}") + + # Получаем дни с заметками + days_with_notes = self.db.get_dates_with_notes(year, month) + + # Создаем календарь + cal = calendar.monthcalendar(year, month) + + for idx, btn in enumerate(self.day_buttons): + row = idx // 7 + col = idx % 7 + + if row < len(cal) and col < len(cal[row]): + day = cal[row][col] + if day != 0: + btn.config(text=str(day)) + + # Определяем цвет фона + today = datetime.now() + is_today = (day == today.day and + month == today.month and + year == today.year) + + is_selected = (day == self.selected_date.day and + month == self.selected_date.month and + year == self.selected_date.year) + + has_notes = day in days_with_notes + + if is_today and is_selected: + btn.config( + bg=Config.COLORS['today_bg'], + fg=Config.COLORS['fg'] + ) + elif is_today: + btn.config( + bg=Config.COLORS['today_bg'], + fg=Config.COLORS['fg'] + ) + elif is_selected: + btn.config( + bg=Config.COLORS['accent'], + fg=Config.COLORS['fg'] + ) + elif has_notes: + btn.config( + bg='#5a7a3a', + fg=Config.COLORS['fg'] + ) + else: + btn.config( + bg=Config.COLORS['calendar_bg'], + fg=Config.COLORS['fg'] + ) + else: + btn.config(text="", bg=Config.COLORS['calendar_bg']) + else: + btn.config(text="", bg=Config.COLORS['calendar_bg']) + + def prev_month(self): + if self.current_date.month == 1: + self.current_date = self.current_date.replace(year=self.current_date.year - 1, month=12) + else: + self.current_date = self.current_date.replace(month=self.current_date.month - 1) + self.update_calendar() + + def next_month(self): + if self.current_date.month == 12: + self.current_date = self.current_date.replace(year=self.current_date.year + 1, month=1) + else: + self.current_date = self.current_date.replace(month=self.current_date.month + 1) + self.update_calendar() + + def on_day_click(self, row, col): + year = self.current_date.year + month = self.current_date.month + cal = calendar.monthcalendar(year, month) + + if row < len(cal) and col < len(cal[row]): + day = cal[row][col] + if day != 0: + self.selected_date = datetime(year, month, day) + self.update_calendar() + + if self.on_date_select: + self.on_date_select(self.selected_date) \ No newline at end of file diff --git a/app/ui/dialogs.py b/app/ui/dialogs.py new file mode 100644 index 0000000..0664ee2 --- /dev/null +++ b/app/ui/dialogs.py @@ -0,0 +1,219 @@ +"""Диалоговые окна""" +import tkinter as tk +from tkinter import ttk, messagebox +from app.config import Config + + +class PasswordDialog(tk.Toplevel): + """Диалог установки/ввода мастер-пароля""" + + def __init__(self, parent, auth, mode='set'): + super().__init__(parent) + self.auth = auth + self.mode = mode # 'set' или 'check' + self.result = False + + self.title("Мастер-пароль" if mode == 'set' else "Введите пароль") + self.geometry("400x250") + self.resizable(False, False) + self.configure(bg=Config.COLORS['bg']) + + self.transient(parent) + self.grab_set() + + self.create_widgets() + + # Центрируем окно + self.update_idletasks() + x = parent.winfo_rootx() + (parent.winfo_width() - 400) // 2 + y = parent.winfo_rooty() + (parent.winfo_height() - 250) // 2 + self.geometry(f"+{x}+{y}") + + def create_widgets(self): + main_frame = tk.Frame(self, bg=Config.COLORS['bg']) + main_frame.pack(expand=True, fill='both', padx=20, pady=20) + + if self.mode == 'set': + text = "Придумайте мастер-пароль\nдля защиты ваших заметок:" + else: + text = "Введите мастер-пароль:" + + tk.Label( + main_frame, + text=text, + bg=Config.COLORS['bg'], + fg=Config.COLORS['fg'], + font=('Arial', 12), + wraplength=350 + ).pack(pady=(0, 15)) + + # Поле ввода пароля + self.password_var = tk.StringVar() + self.password_entry = ttk.Entry( + main_frame, + textvariable=self.password_var, + show='*', + font=('Arial', 12) + ) + self.password_entry.pack(fill='x', pady=(0, 10)) + self.password_entry.focus() + + if self.mode == 'set': + # Подтверждение пароля + tk.Label( + main_frame, + text="Подтвердите пароль:", + bg=Config.COLORS['bg'], + fg=Config.COLORS['fg'], + font=('Arial', 11) + ).pack() + + self.confirm_var = tk.StringVar() + self.confirm_entry = ttk.Entry( + main_frame, + textvariable=self.confirm_var, + show='*', + font=('Arial', 12) + ) + self.confirm_entry.pack(fill='x', pady=(0, 15)) + + # Чекбокс "спрашивать при входе" + self.ask_var = tk.BooleanVar(value=True) + tk.Checkbutton( + main_frame, + text="Спрашивать при входе", + variable=self.ask_var, + bg=Config.COLORS['bg'], + fg=Config.COLORS['fg'], + selectcolor=Config.COLORS['bg'], + activebackground=Config.COLORS['bg'], + activeforeground=Config.COLORS['fg'] + ).pack(pady=(0, 15)) + + # Кнопки + button_frame = tk.Frame(main_frame, bg=Config.COLORS['bg']) + button_frame.pack(fill='x') + + if self.mode == 'set': + tk.Button( + button_frame, + text="Установить пароль", + command=self.set_password, + bg=Config.COLORS['accent'], + fg=Config.COLORS['fg'], + font=('Arial', 11, 'bold'), + relief='flat', + cursor='hand2' + ).pack(fill='x', pady=(0, 5)) + else: + tk.Button( + button_frame, + text="Войти", + command=self.check_password, + bg=Config.COLORS['accent'], + fg=Config.COLORS['fg'], + font=('Arial', 11, 'bold'), + relief='flat', + cursor='hand2' + ).pack(fill='x', pady=(0, 5)) + + if self.mode == 'check': + tk.Button( + button_frame, + text="Отмена", + command=self.destroy, + bg=Config.COLORS['button_bg'], + fg=Config.COLORS['fg'], + relief='flat', + cursor='hand2' + ).pack(fill='x') + + def set_password(self): + password = self.password_var.get() + confirm = self.confirm_var.get() + + if not password: + messagebox.showwarning("Ошибка", "Введите пароль") + return + + if password != confirm: + messagebox.showwarning("Ошибка", "Пароли не совпадают") + return + + self.auth.set_master_password(password) + self.auth.set_ask_password_on_startup(self.ask_var.get()) + self.result = True + self.destroy() + + def check_password(self): + password = self.password_var.get() + + if self.auth.check_password(password): + self.result = True + self.destroy() + else: + messagebox.showerror("Ошибка", "Неверный пароль") + self.password_var.set('') + self.password_entry.focus() + + +class ConfirmDialog(tk.Toplevel): + """Диалог подтверждения действия""" + + def __init__(self, parent, title, message): + super().__init__(parent) + self.result = False + + self.title(title) + self.geometry("350x150") + self.resizable(False, False) + self.configure(bg=Config.COLORS['bg']) + + self.transient(parent) + self.grab_set() + + # Центрируем окно + self.update_idletasks() + x = parent.winfo_rootx() + (parent.winfo_width() - 350) // 2 + y = parent.winfo_rooty() + (parent.winfo_height() - 150) // 2 + self.geometry(f"+{x}+{y}") + + # Сообщение + tk.Label( + self, + text=message, + bg=Config.COLORS['bg'], + fg=Config.COLORS['fg'], + font=('Arial', 11), + wraplength=320 + ).pack(expand=True, pady=(20, 10)) + + # Кнопки + btn_frame = tk.Frame(self, bg=Config.COLORS['bg']) + btn_frame.pack(fill='x', padx=20, pady=(0, 20)) + + tk.Button( + btn_frame, + text="Да", + command=self.confirm, + bg=Config.COLORS['danger'], + fg=Config.COLORS['fg'], + relief='flat', + cursor='hand2', + width=10 + ).pack(side='left', padx=(0, 10)) + + tk.Button( + btn_frame, + text="Нет", + command=self.destroy, + bg=Config.COLORS['button_bg'], + fg=Config.COLORS['fg'], + relief='flat', + cursor='hand2', + width=10 + ).pack(side='left') + + def confirm(self): + self.result = True + self.destroy() \ No newline at end of file diff --git a/app/ui/main_window.py b/app/ui/main_window.py new file mode 100644 index 0000000..4c5fb5f --- /dev/null +++ b/app/ui/main_window.py @@ -0,0 +1,212 @@ +"""Главное окно приложения""" +import tkinter as tk +from tkinter import ttk, filedialog, messagebox +import json +import zipfile +import os +from datetime import datetime +from app.config import Config +from app.database import Database +from app.auth import Auth +from app.ui.sidebar import Sidebar +from app.ui.note_editor import NoteEditor +from app.ui.dialogs import PasswordDialog + + +class MainWindow(tk.Tk): + """Главное окно приложения""" + + def __init__(self): + super().__init__() + + self.title(Config.APP_NAME) + self.geometry(f"{Config.WINDOW_WIDTH}x{Config.WINDOW_HEIGHT}") + self.minsize(Config.MIN_WINDOW_WIDTH, Config.MIN_WINDOW_HEIGHT) + self.configure(bg=Config.COLORS['bg']) + + # Инициализация компонентов + self.db = Database() + self.auth = Auth(self.db) + + # Проверка пароля + if not self.check_auth(): + self.destroy() + return + + # Создание интерфейса + self.create_menu() + self.create_widgets() + + # Загрузка заметок для текущей даты + self.sidebar.refresh_notes() + + # Привязка закрытия окна + self.protocol("WM_DELETE_WINDOW", self.on_closing) + + def check_auth(self): + """Проверка аутентификации""" + if self.auth.is_password_set(): + if self.auth.ask_password_on_startup(): + dialog = PasswordDialog(self, self.auth, mode='check') + self.wait_window(dialog) + return dialog.result + else: + # Предлагаем создать пароль + dialog = PasswordDialog(self, self.auth, mode='set') + self.wait_window(dialog) + return dialog.result + + return True + + def create_menu(self): + """Создание меню""" + menubar = tk.Menu(self) + self.config(menu=menubar) + + # Меню Файл + file_menu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label="Файл", menu=file_menu) + + file_menu.add_command(label="Экспорт в ZIP", command=self.export_to_zip, accelerator="Ctrl+E") + file_menu.add_command(label="Импорт из ZIP", command=self.import_from_zip, accelerator="Ctrl+I") + file_menu.add_separator() + file_menu.add_command(label="Выход", command=self.on_closing, accelerator="Ctrl+Q") + + # Меню Настройки + settings_menu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label="Настройки", menu=settings_menu) + + settings_menu.add_command(label="Изменить мастер-пароль", command=self.change_password) + + # Горячие клавиши + self.bind('', lambda e: self.export_to_zip()) + self.bind('', lambda e: self.import_from_zip()) + self.bind('', lambda e: self.on_closing()) + + def create_widgets(self): + """Создание виджетов интерфейса""" + # Основной контейнер + main_container = tk.Frame(self, bg=Config.COLORS['bg']) + main_container.pack(fill='both', expand=True) + + # Боковая панель + self.sidebar = Sidebar( + main_container, + self.db, + on_note_select=self.on_note_selected, + on_note_delete=self.on_note_deleted + ) + + # Разделитель + separator = tk.Frame(main_container, width=2, bg=Config.COLORS['separator']) + separator.pack(side='left', fill='y') + + # Редактор заметок + self.editor = NoteEditor( + main_container, + self.db, + on_note_saved=self.on_note_saved, + on_note_deleted=self.on_note_deleted, + on_new_note=self.on_new_note_created + ) + self.editor.pack(side='left', fill='both', expand=True) + + def on_note_selected(self, note): + """Обработчик выбора заметки""" + self.editor.show_note(note) + + def on_note_saved(self, note): + """Обработчик сохранения заметки""" + self.sidebar.refresh_notes() + + def on_note_deleted(self): + """Обработчик удаления заметки""" + self.sidebar.refresh_notes() + + def on_new_note_created(self): + """Обработчик создания новой заметки""" + pass + + def export_to_zip(self): + """Экспорт данных в ZIP архив""" + filename = filedialog.asksaveasfilename( + defaultextension=".zip", + filetypes=[("ZIP files", "*.zip"), ("All files", "*.*")], + initialfile=f"captains_log_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip" + ) + + if filename: + try: + # Экспортируем данные + data = self.db.export_data() + + # Создаем временный JSON файл + temp_json = os.path.join(os.path.dirname(filename), 'temp_export.json') + with open(temp_json, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + # Создаем ZIP архив + with zipfile.ZipFile(filename, 'w', zipfile.ZIP_DEFLATED) as zipf: + zipf.write(temp_json, 'captains_log_backup.json') + + # Удаляем временный файл + os.remove(temp_json) + + messagebox.showinfo("Успех", f"Данные экспортированы в:\n{filename}") + except Exception as e: + messagebox.showerror("Ошибка", f"Не удалось экспортировать данные:\n{str(e)}") + + def import_from_zip(self): + """Импорт данных из ZIP архива""" + filename = filedialog.askopenfilename( + filetypes=[("ZIP files", "*.zip"), ("All files", "*.*")] + ) + + if filename: + # Подтверждение + if not messagebox.askyesno( + "Подтверждение", + "Импорт данных заменит все существующие заметки.\nПродолжить?" + ): + return + + try: + # Извлекаем из ZIP + with zipfile.ZipFile(filename, 'r') as zipf: + temp_json = os.path.join(os.path.dirname(filename), 'temp_import.json') + zipf.extract('captains_log_backup.json', os.path.dirname(filename)) + + # Читаем JSON + with open(temp_json, 'r', encoding='utf-8') as f: + data = json.load(f) + + # Импортируем данные + self.db.import_data(data) + + # Удаляем временный файл + os.remove(temp_json) + + # Обновляем интерфейс + self.sidebar.refresh_notes() + self.editor.show_empty_state() + + messagebox.showinfo("Успех", "Данные успешно импортированы") + except Exception as e: + messagebox.showerror("Ошибка", f"Не удалось импортировать данные:\n{str(e)}") + + def change_password(self): + """Изменение мастер-пароля""" + if self.auth.is_password_set(): + dialog = PasswordDialog(self, self.auth, mode='check') + self.wait_window(dialog) + + if dialog.result: + dialog = PasswordDialog(self, self.auth, mode='set') + self.wait_window(dialog) + else: + dialog = PasswordDialog(self, self.auth, mode='set') + self.wait_window(dialog) + + def on_closing(self): + """Обработчик закрытия окна""" + self.destroy() \ No newline at end of file diff --git a/app/ui/note_editor.py b/app/ui/note_editor.py new file mode 100644 index 0000000..c4ba58b --- /dev/null +++ b/app/ui/note_editor.py @@ -0,0 +1,297 @@ +"""Редактор заметок""" +import tkinter as tk +from tkinter import ttk, messagebox +from datetime import datetime +from app.config import Config +from app.ui.dialogs import ConfirmDialog + +class NoteEditor(tk.Frame): + """Правая панель с редактором заметок""" + + def __init__(self, parent, db, on_note_saved=None, on_note_deleted=None, on_new_note=None): + super().__init__(parent, bg=Config.COLORS['editor_bg']) + self.db = db + self.on_note_saved = on_note_saved + self.on_note_deleted = on_note_deleted + self.on_new_note = on_new_note + + self.current_note = None + self.is_new_note = False + self.is_editing = False + + self.create_widgets() + self.show_empty_state() + + def create_widgets(self): + # Панель инструментов + self.toolbar = tk.Frame(self, bg=Config.COLORS['sidebar_bg'], height=40) + self.toolbar.pack(fill='x') + self.toolbar.pack_propagate(False) + + # Дата и время (левый край) + self.date_label = tk.Label( + self.toolbar, + text="", + bg=Config.COLORS['sidebar_bg'], + fg=Config.COLORS['fg'], + font=('Arial', 11) + ) + self.date_label.pack(side='left', padx=10) + + # Кнопки (правый край) + buttons_frame = tk.Frame(self.toolbar, bg=Config.COLORS['sidebar_bg']) + buttons_frame.pack(side='right', padx=5) + + # Кнопка сохранения + self.save_btn = tk.Button( + buttons_frame, + text="💾", + command=self.save_note, + bg=Config.COLORS['button_bg'], + fg=Config.COLORS['fg'], + relief='flat', + font=('Arial', 14), + cursor='hand2', + width=2 + ) + self.save_btn.pack(side='left', padx=2) + + # Кнопка удаления + self.delete_btn = tk.Button( + buttons_frame, + text="🗑", + command=self.delete_current_note, + bg=Config.COLORS['button_bg'], + fg=Config.COLORS['fg'], + relief='flat', + font=('Arial', 14), + cursor='hand2', + width=2 + ) + self.delete_btn.pack(side='left', padx=2) + + # Кнопка создания новой заметки + self.new_btn = tk.Button( + buttons_frame, + text="+", + command=self.create_new_note, + bg=Config.COLORS['accent'], + fg=Config.COLORS['fg'], + relief='flat', + font=('Arial', 14, 'bold'), + cursor='hand2', + width=2 + ) + self.new_btn.pack(side='left', padx=2) + + # Скрываем кнопки сохранения и удаления по умолчанию + self.hide_editor_buttons() + + # Область контента + self.content_frame = tk.Frame(self, bg=Config.COLORS['editor_bg']) + self.content_frame.pack(fill='both', expand=True, padx=10, pady=10) + + # Текстовое поле для редактирования + self.text_widget = tk.Text( + self.content_frame, + bg=Config.COLORS['text_bg'], + fg=Config.COLORS['text_fg'], + font=('Arial', 12), + wrap='word', + relief='flat', + padx=10, + pady=10, + insertbackground=Config.COLORS['fg'] + ) + + # Scrollbar для текстового поля + self.text_scrollbar = ttk.Scrollbar( + self.content_frame, + orient='vertical', + command=self.text_widget.yview + ) + self.text_widget.configure(yscrollcommand=self.text_scrollbar.set) + + # Метка для пустого состояния + self.empty_label = tk.Label( + self.content_frame, + text="Выберите заметку из списка\nили создайте новую", + bg=Config.COLORS['editor_bg'], + fg=Config.COLORS['fg'], + font=('Arial', 14), + justify='center' + ) + self.empty_label.pack(expand=True) + + # Привязываем события изменения текста + self.text_widget.bind('<>', self.on_text_modified) + + def show_editor_buttons(self): + """Показать кнопки редактора""" + self.save_btn.pack(side='left', padx=2) + self.delete_btn.pack(side='left', padx=2) + + def hide_editor_buttons(self): + """Скрыть кнопки редактора""" + self.save_btn.pack_forget() + self.delete_btn.pack_forget() + + def show_empty_state(self): + """Показать пустое состояние""" + self.text_widget.pack_forget() + self.text_scrollbar.pack_forget() + self.empty_label.pack(expand=True) + self.date_label.config(text="") + self.hide_editor_buttons() + self.current_note = None + self.is_new_note = False + self.is_editing = False + + def show_note(self, note): + """Показать существующую заметку""" + self.current_note = note + self.is_new_note = False + self.is_editing = False + + # Скрываем пустую метку + self.empty_label.pack_forget() + + # Показываем текстовое поле + self.text_widget.pack(side='left', fill='both', expand=True) + self.text_scrollbar.pack(side='right', fill='y') + + # Устанавливаем дату + created_at = datetime.strptime( + note['created_at'], '%Y-%m-%d %H:%M:%S' + ) + self.date_label.config( + text=created_at.strftime('%d.%m.%Y %H:%M') + ) + + # Очищаем и вставляем текст + self.text_widget.delete('1.0', 'end') + self.text_widget.insert('1.0', note.get('content', '')) + self.text_widget.edit_modified(False) + + # Показываем кнопки + self.show_editor_buttons() + + # Делаем поле только для чтения + self.text_widget.configure(state='disabled') + + # Привязываем клик для редактирования + self.text_widget.bind('', self.enable_editing) + + def enable_editing(self, event=None): + """Включить режим редактирования""" + if not self.is_editing and self.current_note: + self.is_editing = True + self.text_widget.configure(state='normal') + self.text_widget.unbind('') + + def create_new_note(self): + """Создать новую заметку""" + self.current_note = None + self.is_new_note = True + self.is_editing = True + + # Скрываем пустую метку + self.empty_label.pack_forget() + + # Показываем текстовое поле + self.text_widget.pack(side='left', fill='both', expand=True) + self.text_scrollbar.pack(side='right', fill='y') + + # Устанавливаем текущую дату + now = datetime.now() + self.date_label.config(text=now.strftime('%d.%m.%Y %H:%M')) + + # Очищаем текстовое поле + self.text_widget.delete('1.0', 'end') + self.text_widget.configure(state='normal') + self.text_widget.edit_modified(False) + + # Показываем кнопки + self.show_editor_buttons() + + # Убираем привязку клика + self.text_widget.unbind('') + + # Ставим фокус + self.text_widget.focus_set() + + if self.on_new_note: + self.on_new_note() + + def save_note(self): + """Сохранить заметку""" + content = self.text_widget.get('1.0', 'end-1c') + + if not content.strip() and self.is_new_note: + messagebox.showwarning( + "Предупреждение", + "Нельзя сохранить пустую заметку" + ) + return + + if self.is_new_note: + # Создаем новую заметку + now = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + note_id = self.db.create_note( + title=None, + content=content, + created_at=now + ) + self.current_note = self.db.get_note_by_id(note_id) + self.is_new_note = False + self.is_editing = False + + # Делаем поле только для чтения + self.text_widget.configure(state='disabled') + self.text_widget.bind('', self.enable_editing) + + messagebox.showinfo("Успех", "Заметка сохранена") + else: + # Обновляем существующую заметку + self.db.update_note( + self.current_note['id'], + content=content + ) + self.current_note = self.db.get_note_by_id(self.current_note['id']) + self.is_editing = False + + # Делаем поле только для чтения + self.text_widget.configure(state='disabled') + self.text_widget.bind('', self.enable_editing) + + messagebox.showinfo("Успех", "Заметка обновлена") + + self.text_widget.edit_modified(False) + + if self.on_note_saved: + self.on_note_saved(self.current_note) + + def delete_current_note(self): + """Удалить текущую заметку""" + if not self.current_note: + return + + dialog = ConfirmDialog( + self.winfo_toplevel(), + "Удаление заметки", + "Вы уверены, что хотите удалить эту заметку?" + ) + self.wait_window(dialog) + + if dialog.result: + self.db.delete_note(self.current_note['id']) + self.show_empty_state() + + if self.on_note_deleted: + self.on_note_deleted() + + def on_text_modified(self, event=None): + """Обработчик изменения текста""" + if self.text_widget.edit_modified() and self.current_note and not self.is_editing: + # Текст был изменен программно, игнорируем + self.text_widget.edit_modified(False) \ No newline at end of file diff --git a/app/ui/notes_list.py b/app/ui/notes_list.py new file mode 100644 index 0000000..ba72a82 --- /dev/null +++ b/app/ui/notes_list.py @@ -0,0 +1,230 @@ +"""Список заметок""" +import tkinter as tk +from tkinter import ttk +from datetime import datetime +from app.config import Config +from app.ui.dialogs import ConfirmDialog + + +class NotesList(tk.Frame): + """Виджет списка заметок""" + + def __init__(self, parent, db, on_note_select=None, on_note_delete=None): + super().__init__(parent, bg=Config.COLORS['list_bg']) + self.db = db + self.on_note_select = on_note_select + self.on_note_delete = on_note_delete + + self.notes = [] + self.selected_note_id = None + + self.create_widgets() + + def create_widgets(self): + # Заголовок + self.header_label = tk.Label( + self, + text="Заметки", + bg=Config.COLORS['list_bg'], + fg=Config.COLORS['fg'], + font=('Arial', 12, 'bold') + ) + self.header_label.pack(fill='x', padx=10, pady=(10, 5)) + + # Разделитель + separator = tk.Frame(self, height=1, bg=Config.COLORS['separator']) + separator.pack(fill='x', padx=5) + + # Canvas и Scrollbar для списка + self.canvas = tk.Canvas( + self, + bg=Config.COLORS['list_bg'], + highlightthickness=0 + ) + self.scrollbar = ttk.Scrollbar( + self, + orient="vertical", + command=self.canvas.yview + ) + + self.notes_frame = tk.Frame(self.canvas, bg=Config.COLORS['list_bg']) + + self.notes_frame.bind("", + lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all"))) + + self.canvas_window = self.canvas.create_window( + (0, 0), window=self.notes_frame, anchor="nw" + ) + + self.canvas.configure(yscrollcommand=self.scrollbar.set) + + self.canvas.pack(side="left", fill="both", expand=True) + self.scrollbar.pack(side="right", fill="y") + + # Bind для изменения размера + self.canvas.bind('', self.on_canvas_configure) + + # Bind для колесика мыши + self.canvas.bind_all("", self.on_mousewheel) + + def on_canvas_configure(self, event): + self.canvas.itemconfig(self.canvas_window, width=event.width) + + def on_mousewheel(self, event): + self.canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") + + def load_notes(self, date): + """Загрузить заметки для выбранной даты""" + # Очищаем текущий список + for widget in self.notes_frame.winfo_children(): + widget.destroy() + + date_str = date.strftime('%Y-%m-%d') + self.notes = self.db.get_notes_with_content_by_date(date_str) + + if not self.notes: + empty_label = tk.Label( + self.notes_frame, + text="Нет заметок\nна этот день", + bg=Config.COLORS['list_bg'], + fg=Config.COLORS['fg'], + font=('Arial', 10), + justify='center' + ) + empty_label.pack(expand=True, pady=20) + return + + for note in self.notes: + self.create_note_item(note) + + def create_note_item(self, note): + """Создать элемент списка для заметки""" + item_frame = tk.Frame( + self.notes_frame, + bg=Config.COLORS['list_bg'], + cursor='hand2' + ) + item_frame.pack(fill='x', padx=5, pady=2) + + # Рамка для элемента + content_frame = tk.Frame( + item_frame, + bg=Config.COLORS['list_bg'], + highlightbackground=Config.COLORS['separator'], + highlightthickness=1 + ) + content_frame.pack(fill='x', padx=5, pady=2) + + # Левая часть с датой + date_frame = tk.Frame(content_frame, bg=Config.COLORS['list_bg']) + date_frame.pack(side='left', padx=(10, 5), pady=5) + + # Получаем дату заметки + created_at = datetime.strptime( + note['created_at'], '%Y-%m-%d %H:%M:%S' + ) + + day_month = created_at.strftime('%d.%m') + year = created_at.strftime('%Y') + + tk.Label( + date_frame, + text=day_month, + bg=Config.COLORS['list_bg'], + fg=Config.COLORS['fg'], + font=('Arial', 11, 'bold') + ).pack(anchor='w') + + tk.Label( + date_frame, + text=year, + bg=Config.COLORS['list_bg'], + fg=Config.COLORS['fg'], + font=('Arial', 9) + ).pack(anchor='w') + + # Средняя часть с заголовком + title_frame = tk.Frame(content_frame, bg=Config.COLORS['list_bg']) + title_frame.pack(side='left', fill='x', expand=True, padx=10, pady=5) + + title = note.get('title', '') + if not title: + # Берем первые 22 символа текста + content = note.get('content', '') + title = content[:22] + ('...' if len(content) > 22 else '') + if not title: + title = "Пустая заметка" + + title_label = tk.Label( + title_frame, + text=title, + bg=Config.COLORS['list_bg'], + fg=Config.COLORS['fg'], + font=('Arial', 10), + anchor='w', + justify='left' + ) + title_label.pack(anchor='w') + + # Кнопка удаления + delete_btn = tk.Button( + content_frame, + text="🗑", + command=lambda n=note: self.delete_note(n), + bg=Config.COLORS['list_bg'], + fg=Config.COLORS['danger'], + relief='flat', + font=('Arial', 14), + cursor='hand2', + width=2 + ) + delete_btn.pack(side='right', padx=(0, 10), pady=5) + + # Привязываем события клика для выбора заметки + for widget in [item_frame, content_frame, date_frame, title_frame, title_label]: + widget.bind('', lambda e, n=note: self.select_note(n)) + + # Подсветка при наведении + for widget in [content_frame, date_frame, title_frame]: + widget.bind('', lambda e, f=content_frame: + f.configure(bg=Config.COLORS['list_selected'])) + widget.bind('', lambda e, f=content_frame: + f.configure(bg=Config.COLORS['list_bg'])) + + def select_note(self, note): + """Выбрать заметку""" + self.selected_note_id = note['id'] + + # Обновляем визуальное выделение + for widget in self.notes_frame.winfo_children(): + if isinstance(widget, tk.Frame): + for child in widget.winfo_children(): + if isinstance(child, tk.Frame): + child.configure(bg=Config.COLORS['list_bg']) + + if self.on_note_select: + self.on_note_select(note) + + def delete_note(self, note): + """Удалить заметку с подтверждением""" + dialog = ConfirmDialog( + self.winfo_toplevel(), + "Удаление заметки", + "Вы уверены, что хотите удалить эту заметку?" + ) + self.wait_window(dialog) + + if dialog.result: + self.db.delete_note(note['id']) + + if self.on_note_delete: + self.on_note_delete() + + # Перезагружаем список + if hasattr(self, 'current_date'): + self.load_notes(self.current_date) + + def refresh(self, date): + """Обновить список заметок""" + self.current_date = date + self.load_notes(date) \ No newline at end of file diff --git a/app/ui/sidebar.py b/app/ui/sidebar.py new file mode 100644 index 0000000..6e2814b --- /dev/null +++ b/app/ui/sidebar.py @@ -0,0 +1,59 @@ +"""Боковая панель""" +import tkinter as tk +from app.config import Config +from app.ui.calendar_widget import CalendarWidget +from app.ui.notes_list import NotesList + + +class Sidebar(tk.Frame): + """Левая боковая панель""" + + def __init__(self, parent, db, on_date_select=None, on_note_select=None, on_note_delete=None): + super().__init__(parent, bg=Config.COLORS['sidebar_bg'], width=Config.SIDEBAR_WIDTH) + self.db = db + self.on_note_select = on_note_select + + self.pack_propagate(False) + self.pack(side='left', fill='y') + + self.create_widgets(on_date_select, on_note_delete) + + def create_widgets(self, on_date_select, on_note_delete): + # Календарь + self.calendar = CalendarWidget( + self, + self.db, + on_date_select=self.on_date_selected + ) + self.calendar.pack(fill='x') + + # Разделитель + separator = tk.Frame(self, height=2, bg=Config.COLORS['separator']) + separator.pack(fill='x', padx=10, pady=(5, 0)) + + # Список заметок + self.notes_list = NotesList( + self, + self.db, + on_note_select=self.on_note_selected, + on_note_delete=on_note_delete + ) + self.notes_list.pack(fill='both', expand=True) + + def on_date_selected(self, date): + """Обработчик выбора даты в календаре""" + self.notes_list.refresh(date) + + def on_note_selected(self, note): + """Обработчик выбора заметки""" + if self.on_note_select: + self.on_note_select(note) + + def refresh_notes(self): + """Обновить список заметок для текущей даты""" + if self.calendar: + self.notes_list.refresh(self.calendar.selected_date) + + def get_selected_date(self): + """Получить выбранную дату""" + return self.calendar.selected_date if self.calendar else None \ No newline at end of file diff --git a/captains_log.db b/captains_log.db new file mode 100644 index 0000000000000000000000000000000000000000..0b4fd297c4c90893d2c4563b8c3998f1cf543d03 GIT binary patch literal 20480 zcmeI)&raJg90%~YDdo=uVcH=Fgp^G}0>MVp(0_1Q$FiwfTUbdYI7Oy$Mn_4YamP4L z({|c%&#;%^0d^d>y~3UV?Krp44G`2EdKl8zN*X7ze~mwhttM5w@w(LD%<#k39_5Cm zOe%yZPYpv+l!Tav#N10Uk%{+K#FAXd-;_-#Pu`x4YrmEF*oiWc(Z1h%Br-@4fB*y_ z009U<00IygkidF8q0i5gEl#(aEIMe454Uc_4%#fJUtJwqt=a|0Hk`t_>$*<9kZ#N%-6GE1>5`H|8lJT8aMZGf(+}^)8tcl*3TH{jo?iboiAgT zdPZ++zlhS#vCEG7)$jD5&XpxAbNj9#Jy-8&vwf?pU3N^BHR)kjYNb-YFmsEsguXgW zlt$pO_vb?yZS%0ZE(XA}^W&PmoGOOfk7`8k&4BUv*7H{vHlQDHh`@2KjvYVSEp8hbtMkmy%X4YnfS^N_qKul3Bjx=h9|2 z>su_78xw6(wPQs))_!R}2iHJEf&c^{009U<00Izz00bZa0SG`~AOchB3c2ACf;y?r zkc&?cV!E0hz3%xz=lg%6{ZPb&1OW&@00Izz00bZa0SG_<0uX?}-4IwL;#d0*EbAzI zm`bKHkCS4`8mZKB+FV}BUbE@!{{K|bPVYtoag-2%00bZa0SG_<0uX=z1Rwwb2;5d+ MgsA#x_a96C0HY#XcmMzZ literal 0 HcmV?d00001 diff --git a/main.py b/main.py new file mode 100644 index 0000000..b6d6533 --- /dev/null +++ b/main.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +""" +Captain's Log - Приложение для быстрых заметок +""" + +import sys +import os + +# Добавляем текущую директорию в путь для импорта +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from app.ui.main_window import MainWindow + +def main(): + """Точка входа в приложение""" + app = MainWindow() + app.mainloop() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b819adf --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +tkcalendar==1.6.1 +nuitka>=2.0.0 \ No newline at end of file From 01c1f53a87e1603bbd955b2efae65e1a2b4ae6aa Mon Sep 17 00:00:00 2001 From: Vic Date: Sat, 4 Jul 2026 16:07:24 +0300 Subject: [PATCH 2/4] Initial commit --- LICENSE | 9 +++++++++ README.md | 3 +++ 2 files changed, 12 insertions(+) create mode 100644 LICENSE create mode 100644 README.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..30e523e --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2026 Vic + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5221af0 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Captain_s-Log + +Simple diary app \ No newline at end of file From f23bdd99670d53976c98f06cee844cab0014739c Mon Sep 17 00:00:00 2001 From: Vic Date: Sat, 4 Jul 2026 16:07:48 +0300 Subject: [PATCH 3/4] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20README.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5221af0..a1d4beb 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,16 @@ -# Captain_s-Log +# Captain's Log -Simple diary app \ No newline at end of file +Приложение для быстрых заметок с шифрованием. + +## Установка и запуск + +1. Клонируйте репозиторий +2. Создайте виртуальное окружение: `python -m venv .venv` +3. Активируйте его: `.venv\Scripts\activate` (Windows) +4. Установите зависимости: `pip install -r requirements.txt` +5. Запустите: `python main.py` + +## Первый запуск + +При первом запуске приложение предложит создать мастер-пароль. +База данных создается автоматически в папке приложения. \ No newline at end of file From 9b0a20aa913d9fd6d578da9bb62783c4d9650c4d Mon Sep 17 00:00:00 2001 From: Vic Sergeev Date: Sat, 4 Jul 2026 16:14:16 +0300 Subject: [PATCH 4/4] working --- .gitignore | 35 ++++++++ LICENSE | 9 ++ README.md | 16 ++++ app/auth.py | 22 ++++- app/ui/dialogs.py | 209 +++++++++++++++++++++++++++++++--------------- main.py | 53 ++++++++++++ 6 files changed, 275 insertions(+), 69 deletions(-) create mode 100644 LICENSE create mode 100644 README.md diff --git a/.gitignore b/.gitignore index e69de29..8dd4581 100644 --- a/.gitignore +++ b/.gitignore @@ -0,0 +1,35 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +*.egg-info/ +dist/ +build/ +*.egg + +# Виртуальное окружение +.venv/ +venv/ +ENV/ +env/ + +# База данных и данные приложения +captains_log.db +*.db + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Nuitka +*.build/ +*.dist/ +*.onefile-build/ + +# OS +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..30e523e --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2026 Vic + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a1d4beb --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# Captain's Log + +Приложение для быстрых заметок с шифрованием. + +## Установка и запуск + +1. Клонируйте репозиторий +2. Создайте виртуальное окружение: `python -m venv .venv` +3. Активируйте его: `.venv\Scripts\activate` (Windows) +4. Установите зависимости: `pip install -r requirements.txt` +5. Запустите: `python main.py` + +## Первый запуск + +При первом запуске приложение предложит создать мастер-пароль. +База данных создается автоматически в папке приложения. \ No newline at end of file diff --git a/app/auth.py b/app/auth.py index ef5e3ed..25adb28 100644 --- a/app/auth.py +++ b/app/auth.py @@ -1,9 +1,8 @@ """Модуль аутентификации""" import hashlib -import base64 +import os from app.database import Database - class Auth: def __init__(self, db: Database): self.db = db @@ -34,6 +33,23 @@ class Auth: """Установить, спрашивать ли пароль при входе""" self.db.set_setting('ask_password_on_startup', '1' if value else '0') + def reset_password(self): + """Сбросить мастер-пароль""" + self.db.set_setting('password_set', '0') + self.db.set_setting('master_password_hash', '') + self.db.set_setting('ask_password_on_startup', '1') + + def change_password(self, old_password, new_password): + """Изменить мастер-пароль""" + if self.is_password_set(): + if not self.check_password(old_password): + return False, "Неверный текущий пароль" + + self.set_master_password(new_password) + return True, "Пароль успешно изменен" + def _hash_password(self, password): - """Хешировать пароль""" + """Хешировать пароль с солью""" + # В будущем здесь будет более безопасное хеширование + # Пока используем SHA-256 для простоты return hashlib.sha256(password.encode()).hexdigest() \ No newline at end of file diff --git a/app/ui/dialogs.py b/app/ui/dialogs.py index 0664ee2..50807ce 100644 --- a/app/ui/dialogs.py +++ b/app/ui/dialogs.py @@ -3,18 +3,19 @@ import tkinter as tk from tkinter import ttk, messagebox from app.config import Config - class PasswordDialog(tk.Toplevel): """Диалог установки/ввода мастер-пароля""" def __init__(self, parent, auth, mode='set'): super().__init__(parent) self.auth = auth - self.mode = mode # 'set' или 'check' + self.mode = mode # 'set', 'check', 'reset' self.result = False - self.title("Мастер-пароль" if mode == 'set' else "Введите пароль") - self.geometry("400x250") + self.title("Мастер-пароль" if mode == 'set' else + "Введите пароль" if mode == 'check' else + "Сброс пароля") + self.geometry("400x300" if mode == 'reset' else "400x250") self.resizable(False, False) self.configure(bg=Config.COLORS['bg']) @@ -35,8 +36,10 @@ class PasswordDialog(tk.Toplevel): if self.mode == 'set': text = "Придумайте мастер-пароль\nдля защиты ваших заметок:" - else: + elif self.mode == 'check': text = "Введите мастер-пароль:" + else: # reset + text = "ВНИМАНИЕ!\nЭто сбросит текущий пароль.\nВсе заметки останутся.\n\nДля сброса введите 'RESET':" tk.Label( main_frame, @@ -47,79 +50,30 @@ class PasswordDialog(tk.Toplevel): wraplength=350 ).pack(pady=(0, 15)) - # Поле ввода пароля - self.password_var = tk.StringVar() - self.password_entry = ttk.Entry( - main_frame, - textvariable=self.password_var, - show='*', - font=('Arial', 12) - ) - self.password_entry.pack(fill='x', pady=(0, 10)) - self.password_entry.focus() - - if self.mode == 'set': - # Подтверждение пароля - tk.Label( + if self.mode == 'reset': + # Поле для сброса + self.reset_var = tk.StringVar() + self.reset_entry = ttk.Entry( main_frame, - text="Подтвердите пароль:", - bg=Config.COLORS['bg'], - fg=Config.COLORS['fg'], - font=('Arial', 11) - ).pack() - - self.confirm_var = tk.StringVar() - self.confirm_entry = ttk.Entry( - main_frame, - textvariable=self.confirm_var, - show='*', + textvariable=self.reset_var, font=('Arial', 12) ) - self.confirm_entry.pack(fill='x', pady=(0, 15)) + self.reset_entry.pack(fill='x', pady=(0, 15)) + self.reset_entry.focus() - # Чекбокс "спрашивать при входе" - self.ask_var = tk.BooleanVar(value=True) - tk.Checkbutton( + tk.Button( main_frame, - text="Спрашивать при входе", - variable=self.ask_var, - bg=Config.COLORS['bg'], - fg=Config.COLORS['fg'], - selectcolor=Config.COLORS['bg'], - activebackground=Config.COLORS['bg'], - activeforeground=Config.COLORS['fg'] - ).pack(pady=(0, 15)) - - # Кнопки - button_frame = tk.Frame(main_frame, bg=Config.COLORS['bg']) - button_frame.pack(fill='x') - - if self.mode == 'set': - tk.Button( - button_frame, - text="Установить пароль", - command=self.set_password, - bg=Config.COLORS['accent'], - fg=Config.COLORS['fg'], - font=('Arial', 11, 'bold'), - relief='flat', - cursor='hand2' - ).pack(fill='x', pady=(0, 5)) - else: - tk.Button( - button_frame, - text="Войти", - command=self.check_password, - bg=Config.COLORS['accent'], + text="Сбросить пароль", + command=self.reset_password, + bg=Config.COLORS['danger'], fg=Config.COLORS['fg'], font=('Arial', 11, 'bold'), relief='flat', cursor='hand2' ).pack(fill='x', pady=(0, 5)) - if self.mode == 'check': tk.Button( - button_frame, + main_frame, text="Отмена", command=self.destroy, bg=Config.COLORS['button_bg'], @@ -127,6 +81,99 @@ class PasswordDialog(tk.Toplevel): relief='flat', cursor='hand2' ).pack(fill='x') + else: + # Поле ввода пароля + self.password_var = tk.StringVar() + self.password_entry = ttk.Entry( + main_frame, + textvariable=self.password_var, + show='*', + font=('Arial', 12) + ) + self.password_entry.pack(fill='x', pady=(0, 10)) + self.password_entry.focus() + + if self.mode == 'set': + # Подтверждение пароля + tk.Label( + main_frame, + text="Подтвердите пароль:", + bg=Config.COLORS['bg'], + fg=Config.COLORS['fg'], + font=('Arial', 11) + ).pack() + + self.confirm_var = tk.StringVar() + self.confirm_entry = ttk.Entry( + main_frame, + textvariable=self.confirm_var, + show='*', + font=('Arial', 12) + ) + self.confirm_entry.pack(fill='x', pady=(0, 15)) + + # Чекбокс "спрашивать при входе" + self.ask_var = tk.BooleanVar(value=True) + tk.Checkbutton( + main_frame, + text="Спрашивать при входе", + variable=self.ask_var, + bg=Config.COLORS['bg'], + fg=Config.COLORS['fg'], + selectcolor=Config.COLORS['bg'], + activebackground=Config.COLORS['bg'], + activeforeground=Config.COLORS['fg'] + ).pack(pady=(0, 15)) + + # Кнопки + button_frame = tk.Frame(main_frame, bg=Config.COLORS['bg']) + button_frame.pack(fill='x') + + if self.mode == 'set': + tk.Button( + button_frame, + text="Установить пароль", + command=self.set_password, + bg=Config.COLORS['accent'], + fg=Config.COLORS['fg'], + font=('Arial', 11, 'bold'), + relief='flat', + cursor='hand2' + ).pack(fill='x', pady=(0, 5)) + else: + tk.Button( + button_frame, + text="Войти", + command=self.check_password, + bg=Config.COLORS['accent'], + fg=Config.COLORS['fg'], + font=('Arial', 11, 'bold'), + relief='flat', + cursor='hand2' + ).pack(fill='x', pady=(0, 5)) + + # Кнопка сброса пароля + tk.Button( + button_frame, + text="Забыли пароль?", + command=self.show_reset_warning, + bg=Config.COLORS['button_bg'], + fg=Config.COLORS['fg'], + relief='flat', + cursor='hand2', + font=('Arial', 9) + ).pack(fill='x', pady=(0, 5)) + + if self.mode == 'check': + tk.Button( + button_frame, + text="Отмена", + command=self.destroy, + bg=Config.COLORS['button_bg'], + fg=Config.COLORS['fg'], + relief='flat', + cursor='hand2' + ).pack(fill='x') def set_password(self): password = self.password_var.get() @@ -136,6 +183,10 @@ class PasswordDialog(tk.Toplevel): messagebox.showwarning("Ошибка", "Введите пароль") return + if len(password) < 4: + messagebox.showwarning("Ошибка", "Пароль должен быть минимум 4 символа") + return + if password != confirm: messagebox.showwarning("Ошибка", "Пароли не совпадают") return @@ -156,6 +207,32 @@ class PasswordDialog(tk.Toplevel): self.password_var.set('') self.password_entry.focus() + def reset_password(self): + if self.reset_var.get().upper() == 'RESET': + self.auth.reset_password() + messagebox.showinfo("Успех", "Пароль сброшен.\nПри следующем запуске создайте новый пароль.") + self.result = True + self.destroy() + else: + messagebox.showerror("Ошибка", "Введите RESET для подтверждения сброса") + self.reset_var.set('') + self.reset_entry.focus() + + def show_reset_warning(self): + """Показать предупреждение о сбросе пароля""" + if messagebox.askyesno( + "Сброс пароля", + "Вы уверены, что хотите сбросить пароль?\n\n" + "Это действие удалит только пароль, все заметки останутся.\n" + "После сброса вы сможете установить новый пароль." + ): + self.destroy() + reset_dialog = PasswordDialog(self.master, self.auth, mode='reset') + self.master.wait_window(reset_dialog) + if reset_dialog.result: + self.result = True + self.destroy() + class ConfirmDialog(tk.Toplevel): """Диалог подтверждения действия""" diff --git a/main.py b/main.py index b6d6533..361baef 100644 --- a/main.py +++ b/main.py @@ -11,10 +11,63 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from app.ui.main_window import MainWindow + +def create_gitignore(): + """Создать .gitignore если его нет""" + gitignore_path = os.path.join(os.path.dirname(__file__), '.gitignore') + + if not os.path.exists(gitignore_path): + gitignore_content = """# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +*.egg-info/ +dist/ +build/ +*.egg + +# Виртуальное окружение +.venv/ +venv/ +ENV/ +env/ + +# База данных и данные приложения +captains_log.db +*.db + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Nuitka +*.build/ +*.dist/ +*.onefile-build/ + +# OS +.DS_Store +Thumbs.db +""" + try: + with open(gitignore_path, 'w') as f: + f.write(gitignore_content) + except: + pass # Игнорируем ошибки при создании .gitignore + + def main(): """Точка входа в приложение""" + # Создаем .gitignore при первом запуске + create_gitignore() + app = MainWindow() app.mainloop() + if __name__ == "__main__": main() \ No newline at end of file