From ed95fff979a3da8c7bd8fb0db454f32413a6844c Mon Sep 17 00:00:00 2001 From: Okami Date: Sat, 31 May 2014 12:00:46 +0400 Subject: [PATCH 1/5] PyQt4 frontend, debian package --- MANIFEST.in | 8 + Makefile | 8 + debian/changelog | 5 + debian/clean | 1 + debian/compat | 1 + debian/control | 12 + debian/copyright | 0 debian/links | 1 + debian/loxodo.desktop | 10 + debian/loxodo.install | 2 + debian/rules | 12 + debian/source/format | 1 + loxodo.py | 52 +- resources/icons/contact-new.svg | 606 ++++++++++++++ resources/icons/document-new.svg | 448 +++++++++++ resources/icons/document-open.svg | 535 ++++++++++++ resources/icons/document-properties.svg | 576 +++++++++++++ resources/icons/document-save-as.svg | 663 +++++++++++++++ resources/icons/document-save.svg | 619 ++++++++++++++ resources/icons/edit-copy.svg | 328 ++++++++ resources/icons/edit-delete.svg | 896 +++++++++++++++++++++ resources/icons/folder-new.svg | 452 +++++++++++ resources/icons/folder.svg | 424 ++++++++++ resources/icons/internet-web-browser.svg | 982 +++++++++++++++++++++++ resources/icons/text-x-generic.svg | 548 +++++++++++++ resources/loxodo-qt.ico | Bin 0 -> 353118 bytes resources/loxodo-qt.svg | 529 ++++++++++++ resources/qt-bw.svg | 618 ++++++++++++++ resources/qt.svg | 535 ++++++++++++ setup.py | 146 ++-- src/config.py | 69 +- src/frontends/qt4/__init__.py | 0 src/frontends/qt4/favicon.py | 147 ++++ src/frontends/qt4/loadframe.py | 163 ++++ src/frontends/qt4/loxodo.py | 48 ++ src/frontends/qt4/recordframe.py | 202 +++++ src/frontends/qt4/settings.py | 161 ++++ src/frontends/qt4/vaultframe.py | 618 ++++++++++++++ 38 files changed, 10349 insertions(+), 77 deletions(-) create mode 100755 MANIFEST.in mode change 100644 => 100755 Makefile create mode 100755 debian/changelog create mode 100755 debian/clean create mode 100755 debian/compat create mode 100755 debian/control create mode 100755 debian/copyright create mode 100755 debian/links create mode 100755 debian/loxodo.desktop create mode 100755 debian/loxodo.install create mode 100755 debian/rules create mode 100755 debian/source/format create mode 100755 resources/icons/contact-new.svg create mode 100755 resources/icons/document-new.svg create mode 100755 resources/icons/document-open.svg create mode 100755 resources/icons/document-properties.svg create mode 100755 resources/icons/document-save-as.svg create mode 100755 resources/icons/document-save.svg create mode 100755 resources/icons/edit-copy.svg create mode 100755 resources/icons/edit-delete.svg create mode 100755 resources/icons/folder-new.svg create mode 100755 resources/icons/folder.svg create mode 100755 resources/icons/internet-web-browser.svg create mode 100755 resources/icons/text-x-generic.svg create mode 100755 resources/loxodo-qt.ico create mode 100755 resources/loxodo-qt.svg create mode 100755 resources/qt-bw.svg create mode 100755 resources/qt.svg create mode 100755 src/frontends/qt4/__init__.py create mode 100755 src/frontends/qt4/favicon.py create mode 100755 src/frontends/qt4/loadframe.py create mode 100755 src/frontends/qt4/loxodo.py create mode 100755 src/frontends/qt4/recordframe.py create mode 100755 src/frontends/qt4/settings.py create mode 100755 src/frontends/qt4/vaultframe.py diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100755 index 0000000..a45fb4d --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,8 @@ +recursive-include locale * +recursive-include resources * +include src/frontends/ppygui/README.txt +include loxodo.py +include LICENSE.txt +include .project +include .pydevproject +include Makefile diff --git a/Makefile b/Makefile old mode 100644 new mode 100755 index 0272215..a1074bc --- a/Makefile +++ b/Makefile @@ -11,3 +11,11 @@ exe: rm -fr build dist python setup.py py2exe +sdist: + python setup.py sdist -d .. + +xdg: + xdg-desktop-menu install --mode system --novendor loxodo.desktop + +clean: + rm -fr build loxodo.egg-info diff --git a/debian/changelog b/debian/changelog new file mode 100755 index 0000000..255cf47 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +loxodo (1.0) unstable; urgency=low + + * source package automatically created by stdeb 0.6.0+git + + -- Christoph Sommer Wed, 07 May 2014 18:51:52 +0400 diff --git a/debian/clean b/debian/clean new file mode 100755 index 0000000..98279cf --- /dev/null +++ b/debian/clean @@ -0,0 +1 @@ +loxodo.egg-info/* diff --git a/debian/compat b/debian/compat new file mode 100755 index 0000000..7f8f011 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +7 diff --git a/debian/control b/debian/control new file mode 100755 index 0000000..a18aa0c --- /dev/null +++ b/debian/control @@ -0,0 +1,12 @@ +Source: loxodo +Maintainer: Christoph Sommer +Section: utils +Priority: optional +Build-Depends: python-setuptools (>= 0.6), python (>= 2.7), python-qt4 (>= 4.10), debhelper (>= 7.4.3), xdg-utils (>= 1.1) +Standards-Version: 3.9.4 +X-Python-Version: >= 2.7 + +Package: loxodo +Architecture: all +Depends: ${misc:Depends}, ${python:Depends} +Description: Password Safe V3 compatible Password Vault diff --git a/debian/copyright b/debian/copyright new file mode 100755 index 0000000..e69de29 diff --git a/debian/links b/debian/links new file mode 100755 index 0000000..bfadc13 --- /dev/null +++ b/debian/links @@ -0,0 +1 @@ +/usr/share/loxodo/loxodo.py /usr/bin/loxodo diff --git a/debian/loxodo.desktop b/debian/loxodo.desktop new file mode 100755 index 0000000..367e523 --- /dev/null +++ b/debian/loxodo.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Name=Loxodo +Comment=Password Safe V3 compatible Password Vault +Keywords=encryption;security; +Exec=/usr/bin/loxodo +Terminal=false +Type=Application +Icon=loxodo-qt +Categories=Utility;Security; +StartupNotify=true diff --git a/debian/loxodo.install b/debian/loxodo.install new file mode 100755 index 0000000..9b6ab81 --- /dev/null +++ b/debian/loxodo.install @@ -0,0 +1,2 @@ +debian/loxodo.desktop /usr/share/applications +resources/loxodo-qt.svg /usr/share/pixmaps diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..5e3ab04 --- /dev/null +++ b/debian/rules @@ -0,0 +1,12 @@ +#!/usr/bin/make -f +export DH_VERBOSE=1 + +%: + dh $@ --with python2 + +override_dh_auto_install: + $(MAKE) -C locale + python setup.py install --root=debian/loxodo --install-layout=deb --install-lib=/usr/share/loxodo --install-scripts=/usr/share/loxodo --install-data=/usr/share/loxodo +# $(MAKE) xdg + +override_dh_auto_build: diff --git a/debian/source/format b/debian/source/format new file mode 100755 index 0000000..163aaf8 --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/loxodo.py b/loxodo.py index 3352105..6d90f85 100755 --- a/loxodo.py +++ b/loxodo.py @@ -1,9 +1,9 @@ #!/usr/bin/env python import sys -import os import platform + # On Windows CE, use the "ppygui" frontend. if platform.system() == "Windows" and platform.release() == "CE": from src.frontends.ppygui import loxodo @@ -13,25 +13,41 @@ from src.config import config # store base script name, taking special care if we're "frozen" using py2app or py2exe -if hasattr(sys,"frozen") and (sys.platform != 'darwin'): +if hasattr(sys, "frozen") and (sys.platform != 'darwin'): config.set_basescript(unicode(sys.executable, sys.getfilesystemencoding())) else: config.set_basescript(unicode(__file__, sys.getfilesystemencoding())) -# If cmdline arguments were given, use the "cmdline" frontend. -if len(sys.argv) > 1: - from src.frontends.cmdline import loxodo - sys.exit() - -# In all other cases, use the "wx" frontend. -try: - import wx -except ImportError, e: - print >> sys.stderr, 'Could not find wxPython, the wxWidgets Python bindings: %s' % e - print >> sys.stderr, 'Falling back to cmdline frontend.' - print >> sys.stderr, '' - from src.frontends.cmdline import loxodo - sys.exit() - -from src.frontends.wx import loxodo +frontends = list(config.FRONTENDS) +# change frontends priority using config +if config.frontend in frontends and frontends.index(config.frontend) > 0: + frontends.remove(config.frontend) + frontends.insert(0, config.frontend) + + +for frontend in frontends: + # update current frontend + config.frontend = frontend + if frontend == 'wx': + try: + import wx + from src.frontends.wx import loxodo + sys.exit() + except ImportError as e: + print('Could not find wxPython, the wxWidgets Python bindings: %s' % e) + print('Falling to the next frontend.') + print('') + elif frontend == 'qt4': + try: + import PyQt4 + from src.frontends.qt4 import loxodo + sys.exit() + except ImportError as e: + print('Could not find PyQt4, the Qt4 Python bindings: %s' % e) + print('Falling to the next frontend.') + print('') + + +from src.frontends.cmdline import loxodo +sys.exit() diff --git a/resources/icons/contact-new.svg b/resources/icons/contact-new.svg new file mode 100755 index 0000000..1efb895 --- /dev/null +++ b/resources/icons/contact-new.svg @@ -0,0 +1,606 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + New Contact + + + address + contact + e-mail + person + information + card + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/document-new.svg b/resources/icons/document-new.svg new file mode 100755 index 0000000..1bfdb16 --- /dev/null +++ b/resources/icons/document-new.svg @@ -0,0 +1,448 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + New Document + + + Jakub Steiner + + + http://jimmac.musichall.cz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/document-open.svg b/resources/icons/document-open.svg new file mode 100755 index 0000000..55e6177 --- /dev/null +++ b/resources/icons/document-open.svg @@ -0,0 +1,535 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Folder Icon Accept + 2005-01-31 + + + Jakub Steiner + + + + http://jimmac.musichall.cz + Active state - when files are being dragged to. + + + Novell, Inc. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/document-properties.svg b/resources/icons/document-properties.svg new file mode 100755 index 0000000..c57f96d --- /dev/null +++ b/resources/icons/document-properties.svg @@ -0,0 +1,576 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Document Properties + + + document + settings + preferences + properties + tweak + + + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/document-save-as.svg b/resources/icons/document-save-as.svg new file mode 100755 index 0000000..01e2fb7 --- /dev/null +++ b/resources/icons/document-save-as.svg @@ -0,0 +1,663 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Save As + + + Jakub Steiner + + + + + hdd + hard drive + save as + io + store + + + + + http://jimmac.musichall.cz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/document-save.svg b/resources/icons/document-save.svg new file mode 100755 index 0000000..2922c43 --- /dev/null +++ b/resources/icons/document-save.svg @@ -0,0 +1,619 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Save + + + Jakub Steiner + + + + + hdd + hard drive + save + io + store + + + + + http://jimmac.musichall.cz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/edit-copy.svg b/resources/icons/edit-copy.svg new file mode 100755 index 0000000..f4d9e97 --- /dev/null +++ b/resources/icons/edit-copy.svg @@ -0,0 +1,328 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Edit Copy + 2005-10-15 + + + Andreas Nilsson + + + + + edit + copy + + + + + + Jakub Steiner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/edit-delete.svg b/resources/icons/edit-delete.svg new file mode 100755 index 0000000..69281e4 --- /dev/null +++ b/resources/icons/edit-delete.svg @@ -0,0 +1,896 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Delete + + + + Jakub Steiner + + + + + edit + delete + shredder + + + + + Novell, Inc. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/folder-new.svg b/resources/icons/folder-new.svg new file mode 100755 index 0000000..0791887 --- /dev/null +++ b/resources/icons/folder-new.svg @@ -0,0 +1,452 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + New Folder + + + + Jakub Steiner + + + + http://jimmac.musichall.cz + + + folder + directory + create + new + + + + + Tuomas Kuosmanen + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/folder.svg b/resources/icons/folder.svg new file mode 100755 index 0000000..79b25c3 --- /dev/null +++ b/resources/icons/folder.svg @@ -0,0 +1,424 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Folder Icon + + + + Jakub Steiner + + + + http://jimmac.musichall.cz + + + folder + directory + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/internet-web-browser.svg b/resources/icons/internet-web-browser.svg new file mode 100755 index 0000000..d2366a9 --- /dev/null +++ b/resources/icons/internet-web-browser.svg @@ -0,0 +1,982 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Globe + + + Jakub Steiner + + + + + Tuomas Kuosmanen + + + + http://jimmac.musichall.cz + + + globe + international + web + www + internet + network + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/icons/text-x-generic.svg b/resources/icons/text-x-generic.svg new file mode 100755 index 0000000..532f98b --- /dev/null +++ b/resources/icons/text-x-generic.svg @@ -0,0 +1,548 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Generic Text + + + text + plaintext + regular + document + + + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/loxodo-qt.ico b/resources/loxodo-qt.ico new file mode 100755 index 0000000000000000000000000000000000000000..1fb472f1e91e8dffe9629fdf6f6913ed4a3af11f GIT binary patch literal 353118 zcmdR12UrzH*EZEejlK6?QL&-Gd;j+g?)@%sK`_Se|9GBZcK7b?%+5P==CnDJDbrV(zRQ#|XC~$)GkJZRDU(B{ zOqoiSOgvx2>pOVO*!aVFxlEaQpLb z$}5T2nHl^8kV#UhQl%=@tXXq+{rdG$zkwOn?R$tFCtf5;F7CcCF)@+!xN_ynKH9Td zUank)uEsTMzG!G+jiKX~BzZP;cp@N(CjFy)xSFTd!YQ|5Rwd(|% zZhiJHojT7aN+FQvy!tuN9?`(qC-#=LSo}^ z<>7Cby=Wsg?mmw5ckW~Oq8VQl&~v?Yi~e^_{&5E}_vl z@ykQpdj1x-{(6hsAJWO&kKjFeHiD)sfphQv;4{C-^Zfh1TD5BT&05&Ncg{A1#LdN# zyDxC){%h>L`6mwDd#%zz<`Y)#!mt&4;1xL$Z0GkzMn+x*3l=nHd%m^k=!USpcM%>t z0qYaOapu&|xXwCUefkPJ*3ZYO)5kDp^Il9l@(XOd`oql340Y<%36MI}s#WWD^A64k z+jtrQBPSr-&mLQMZo{ej&#_?BK12q)A#PGEx`&U!&}F;Ow(l@lSXjIw-WKva%Rkc8 zrUST?hL&?c2A1 z#&Z^B{(PhE=6UAw`> z#RYBKw!JLR634==TesfRu3bAgI=jB_9h-nnmmk7!>>~7?xe-z8PGRJxvluXO4jj9< zt7qkTe*Ybo|Bbb^^?EsXa&l6sv!fH-dIzD$=mZQ~cM^f|%it9p0Z0BW_i4%5*%=KQ zG_Yg2f6B9R-NC^@>ZOi5J3696XD7&g`p>Z4bmwi_v{@u=z`k6T{mD%5U}|e_ZXRJ{ zV{=K~E6>~6+1=;)x`quKZtU2x;|uM5ojP@TE^UxnDqp_5G5eSa%lKNmr)t%zC0ev- zF_iDTC-1rWO{PpwGiS;q`L3@>R}3V%uKkvY$MW}z3H&E?rX*7nCZRv6{b}Xv=Nz-4 z?8oou8S!P4x?%Qu9*#E(HQ}58;#rMAByCU?Pye?vWyB zX@l47SCjS2NbR_6*|JgPDpY*q+_yg(*}I~il^yK-2f@^;4QA~+iHMl-*n8>%j$Xco zv$yZz%*{K9pSuQ2w_a2AXxAeM)?NKbYZB1H!42))yitdJn0@G3v0}v-zN@z{bM|gZA8prL!kGhs9#Wwmo?G>Yc&# z=C&GC3KVFn-v-3|jL;aNQ&no#MTY^=@LzEhc0GI%w{kmIYu4f%8_zWwY23&X9iygV(C*u? z_v)?QzwgQe@*Hn*;={Y;{fF-SrOJ_GN&3IZ`PlhukZ|%o`c9b3{xc0`RyL}9q06La zr?v9g=Y2>g-)ZGH>)Zo=%MM}a@ju})U@&|`BQY~>I9AV%#>TZvaEWcMpDyxz=e|AI zx@I1B@7#jxPhaCA>lzh57qd>>!`N*n(IsRQ>NRhr$`_ig$g@W#()&f@mVce-@s!X>Pk8i0i1u2?&@4_2*S zi5t&fBPeDvW*)zTDF?5iwP!yxY;CW~=bZA6~_w=>! zkDUo$UmuM2sRK{zycpiM6;9sz?LYmI{c_cgJs1+Fxs~PrcR4T!pe>48af&? zj@?H5fh(}|2tpG(7sVeoZQArT%UY|IFFEJfD}sx(K}*jdgzUV5k?Rhk=g4?O$0cB3 zXb5_Fx?<3<(KvSNIeEyxF?;Q942+3Gz>rb!>gk1EU2V}Xz!&|(hoO7FaPoKK6`$lW zcr3b%T?TVUPclOK}nb>jmH~f74r7rDapB|O4 z919Pe#n`!PF=g3yj9<7OBW5naXyHjmP3D*zhh8xW=n^&tF`JITYx-6+BON7wr}!+= zxOw{JvkyA8YSrp*;Wb(ktDrfXRr$SVZa~O{g_yv(CN^OmRv);D;nNpm^ujfWoJ#t) zc|UgC`2#by{fzDdBH=_j7ZAS`L&;xu@Ee5jJ5OWcu5<95wiyi?H`DQWZ0m9QX&RlW~-)xhcNbl*N8W(vkF+lZZa{>0vgFR}38Sqz*s2kwJNVuog4e^ST+8~Z9fHzj$L(S3x7u*&{w~FIVSCmA+P$*#G*BV=Ouo>H_Pw8 z@gjNuGl&>J6Z7|-Qsqk7Bj&G$=g{fsGkq`IhR%c6*d+*@xd}bTFNMdT7}A~Ts{FR@ z-f$$}tMR8S<9(hydFm%GKVQCl)yRLoXxO;P-+gDUQQz-9W4#&+eWIsg*|CeN>@7DR z!*g&fg60y}F{@zLX9!$KuiEqqgG)ploGDMtIeZf#vsS?~Vl>DP>&lioun*j0dw-X_ ze99XYNtf=(JcT)73# zgC--AJXXkxBj_?{tgh^dhefWN3-`b<*mq2dwf=jB?r431eLY&rr<`|F{~rBgO@5PW z?d;masz5_tAX~OQBOn(@^qj6GRqcpGdQ=`tP>1w1l*u ze5d|({bTmCH~Qn?W5>d~lXpsBA3n`zU+ce1zLE3CbIz+fS?{RaxpQ}-4C%%BWFcky zRO|h*GE@6KXU?2eIgb0XeTGn`>_z#m<+tB{o6pewY+EbRwXN(sZ}jUd$A<3zx2_Rq z8}dGDNo!te^>|BN%lk~3a!_N|n0X1F=b-vPQs$>jH~G7yU70d{m0IHOU&%d^_>4+& zyo)J26ori|(-w02^jZOiZaRpTG^bCIP)e;|qX zASgU8iL9#WE7FsXj$fzrAi~3N>?&SOzbyS@89!S6(vz2>o|k=T1Lc!<+IvLjAob>W zRB}t*dWQAC>8ZQiv+dY3m^HRg@3ghGRqxfx(sH)f@2q>D^winVJta$)Y{u_c$@jdG zdTVu;_qVcj{G_%QeUA2xt=c*(9$MOk`bEmr)#Us=dDr7I7CGk28>&}EuCXhA&-Yv^ zSH8T``$*kIZfw!U5vz9H(bYw()5r4R6INpMl=Wz7>x8mp%WE=-(yOsgKIdHACL{Ix z*u6!H7CTtQ$mngY2Bxa4D%Gkh9Z*H;kgC+Ihi<(iGV~4o$!ArrWs1sGjbU7?4yqd0 zqzqk~{m2O9nci{U*dxRDWA#eU?}Z8#8eX}I(K~aSwy4YgPi#@7R9PNVF6z-2r7KjT z9-}!XFW8svZ?Lj;MJ@BzFtM^lnW{BWr(qL_Eu>hfGHNW49=@lpH#|M{OY0u;)ZI9C zUKTA`2BC3NP@TGtN;PVsx@i;2KJGAWuTX%LrpW)FcT&5yST6Khxi5bGzA4Su)_UPO*06QPNfsI2~gbo{z#p}1=%A+T^ zc>fV5%v^v-${g|Yb|?9c&h7!|zi2m1Z8|G?+@!HN?L}SDyuC9Tw``}`uHKQR9%esM zwg;_V>B;%bPn?6VR;*IxoeAwd?!h5w%J!~7%4sngqe^c3#De50~`$#jla1Q&+%FZ4p*6rXFI1sM1(^a7!*D)d< zZX@DhD?F}8FN}(xj{7hER{F-v4}OJ5j}VL=y$d5oZb1-rOro2VbnEFegpU}DiIh{i z^c;i{JFlQgmjHC69d6$F$Ea`F1{R&Wqn&$CShnk^=mgvUJ>}wCY|pIeX?MAYe2*__ z{9husJ9v1ZL96y~3Xg%6Pbiu=xT8^LPlT;K1n)sZF=hTj{QCN>E?uDg&&PKd=FGW? zS+lOdzu$1(Z`yq;wroeoF1;~&?=NWSI~=XN2Pm1=XY^FG?=u)Su3m5ticoZ-Bxx(_ z@tS;MY9->|E5IFesNATELN-twn zyT0Nps4M?HHC|HxyV!5`Qg(b@mu)X|REKWfXimMi=i;6G>xex-gk zMPmJbWzuxVY31v7?SqA*7upY-iqVAUM`I>cG zs9)FQ$I@>&Z@dzotaOFSs{ZDrFI~nhR`oZrwnfOA(`ePT7mnY!muUAD+i&tjJFwV; z^=-nMZ8&+pWXo%+6Ke+P!~`A>BZmQ;8I$NI>bxK(q*0- zO1+0oq@6cF)qmc(-_Y8v7yC~F8gtxr3mK`#fy|k#+h61>lpQ*G+f&aniWt3=x=X4~ zeRECD>kZm>QEl%zV-qawJE7ai1Z+EZSLu~Sx2&a;JWezH%HNqz8AyAsJVK*(POAQb zf!Id^Vy3~8^GPfE5NLJcJo=V2eNFPZ%Q@w)+x%U3Z{zl^sNKYpG$$;v{?j+Y-m@2a zMNYtB+WW=kpH`Z(YDePlAAABd-vBKa9|8SjOM6nnsr%|1I`$ca;d9ra6>+FT{|v4E z!k=;+-(_D(GM{D7p52W7{*CbV+Px;tTcd%E6Z}^mQ}ypTe;2&Nh9Ej&nQF%kv}HW_ z>m6=T&n|5$ZJVBS>NjG>dy}V_IV+S-RNCL2w0hF|pV6pe5Al7_)k*44-ORfmfBdn5 zJ_eK_<2gQbwqf}OV-r)DckQcm_CkC5EI$Iz&G`!sIa#h>M{;WbQ0n zeEbp@Xdg(Ce&;o&>+JihH>^k8_))lU`6?c~c!%@6uBCyaCSdrgy{i3(E#C=GeuG`e z1k#?`y8281AYX^fnKPT}*PnAk499ko`iq^|`a|E5`VZK86`j2OVDI6LHg@f>bHN}S zULKCU3kTxBvIxwWHBI?|h#yGubdA?S`*~yU(qXu|dmN`^Z zzN?O-W1oTO-eLLL)yl@QeP%EC zyEej(Sw0vM(j7~dE~Wp?UC2G&Lnop)ZCcWQga!#uI$+}!7@IcLwZGW7$Pc|>{Y&fD zpZ(vOveav_0cmxYyop;MRsYDNzr!(PG$!vkhpKhzW8{!vOdr?`K@O$S%eDlj4QPVF zq5inWv2g9rlxbSREX0Sj#N;3|v~5@vQ~Ec?fSzq};J^XOV6(7t*I^7ZO@gQ|b+W{LJG%8{Vn?LF-@Pgwtg`t|2|R*v0=GCuU{Z`CVA)!${rOw2odU)8@N z<)r}w2Ex}ryf3Vwrh$omoo5clM@4+ zD`WJ~P;_zkK;!0C=o>X2laD;2zIC>){uXY2AL?(d>Ll}*=yzDZ2dr-re=PYH{=S9t zgO2{oceL~ytm@x`dVaC{1WcNbwFfR>aO_n01Pp|C?>^|!yDuipU5x`*pWwv3zj2aj z|D|8ipT13j`Kp7UR&(S1KL6@ltoJT^qysKa9Hd-S?iOXUI@&qrHCIoV5sxoxyLKgpe^)F<=z! zYf-VZ!}_D6ZwP&@;t;mv0GvZ(bYs|lz-U$fnvE>PzOK`El9#xy-*>cQj)CUvU$6A% zu$I0N=saYSs=Ktmb5tC9M8;vyiGS$|jB`K9iX9HZjlG-NX7Za9kQsmtLvXfz@x%*TqoR}e({ zF>l{V3>-fl!L-qCzV=90SE=)yJ)~dWedzCD12gjDT_PqRX4iG4?|0{W-3AO(bVtUn z%)=c*#-Vkep)faZuB*HBA7ZgNdA+rB@<1cRzMzd`Yx-EY4Nt89vTJ|BarjIGZ@hq! z(^ujEZJ$~iGkZ0HIR8l9x6%%#wXx*OPh5c)X}hCe2-^1!r_4E=ve0y(AjhYEJvVc7QHY3Eu4 zhyLT?IyfF-%MS9JLb2rRugWGZZ7*r?k}asy)UbVmx}1>Y_0~V<_pO$1WgmK9pSl6J zVTtu$dGiH2)1D}ES$E08h5fL37eCHZonhx0h@R0?RNX}et3x?nvq@>~E;(t?TgrzG_3NAb znEIZsv_rfT8%L#THQ+LIvabJkj*M6Jm-(zOeRsOkmq@c2Yi%DEKL^%b`XFk{ud43t z{6?T&>(1y9Jc_dWApHg}DX4 z!;?CTQIvmNBB#J9jAMj8r(&CQ9YlHS*c~h-ui#C8BpI_U99+=ZfPGZMfb}|{UsprN z)U`N@{I1!U%v)HKW{y$y?>c<4s(S!=p}{j3C#ieLgqi3Ovl1c8ub^>fZ}gnJ3bFi7 zzgb&hA2b57dv5A%o58a;W5kl}s_qNu%hHX$6ylqeR^8s{v!F%gr&5(uSBo--LN3fJ^JutjM#e*ExHB4e=2=3 z*xp(ixBss4eUf&U|2WAu(9Q zz9Mz^A2kI{n^q-aSFZzA`YpvSap$`2p z)?d1(tH0}jQLqo50Ix|aRNceZpM#ZKZ}gkBF-6_|<5yzLn*GWTqC>adu=&8#=$B#W zSoCO8AIe^T(l*;zukK%w?^%IgK>+Ul|&RUL6AyeU(uwB)C z$d)T;-7P@*&TH+h<^5-G!9ePU2BfIjIl>{oA- zKcjzcXiO0+v~1UbzEf_n^^H_@kJ@i)J;FDU*iY0|>v@BWl}Cx3?Dk(oSsFy+qe^vy|?o=Mxv*kS+s zmGaSck(H=N)9G>eo^PnX_hWmUP4C>0ye|LroIca#*cXRThM3OpShHC(`FmFKGUGY--~L#C(w_PMm=l|V^q-wXx_vb@JpMn{J9(Xm$4|s` z7-`dK_P=-fV?eFw%R%3v2;NnK`Az1HnbYCuEB>g?CEvyCS{}~Bh!6KMll$r8q@Gtf z-$#EZdOIbqo65xXR`h$~DB;?x5BW{WxdtnFzU#wvEl>V?YUk>`d|&chD_6@^y;zUr zx%RtyKYv%{Gmpr`KfYJ}Y59jWVafwbe$dW&xJ%CYN7m*;elv0H7+#4F7_SRDAM#y^ z>&YT`F7Nm%Q%N~8_$N<&uJqGSKUF6Exgq+!&y_@0A?;9Wv&7GrGCnvz>E561ryQ8R z-#5T-c`f zJ=C$tx}b}n<2%H@#XcqWY31Kc`3mG)bZsT=ChhvA@yuv>;>Y)6maNL3k@Fv@Pu9t3 z!XN$c!w-sw{~T>5?+|-3`JcJz#q4w6$1z%zJm-4aU6R=CGV*<5ZzWG4dKsnPkTxq- zs+10Uc~{AjrC{sO8Mn>Am(7o~w@{HlbJ~z&3y^rO8&F_rj7=2=>o@rf^Z{Qj;MdyWor|&k{pHj3?FL)2IoSzM0EW2R^qS^JqKOEzph`se=y{!2DrCFWUl-yvh3a)55_lE?w_tVkwe%t?lI_5mylyqq&Wzc7-eT!gEorTaZrBjt-%D)ENL+EFt=57Bc zu-Ev7qQ%Q7d$acaGM~veYO<{8r-av)IZwMMtz6oRvh;n<^|ACnOyM{1{|SGsF6;;O zIi8oX|36LsZ9@O}EedyNcjEa<;%6MJ;Nx^7zd zIW8vhw_5*8Sf^E2(!0t`mpJ#mm-$|3zs!TFVK40=^s!pa1}WM&d7J+?&I7q#&BngF zu5K%K+@g%q9oc@$i9Gr8gL?TSew*?=vgS(YzP_$n<~Ppguc?pUL(F6Odo%KNpJmNX zT6lcv-)OHJLD}bpe7o>I>5+qkkM12Dqif^;2F|IK*_}2|=kEO>YvBAtClL1>C|;tZ zvTq62T<2rc8hn?;DP=(EJmsZZCn?-H3; zbvj0OA>qLOJVeHl|$?+uwOqDd$zINze4dP$0NHG*FlBffaxl-h&BE^cMq6ybh(uY^(%AC1!BYTcqux#y!?Z=+~ z*Jwe#hStcQGY`J`=3D%jH5>bMe)Ub+a^yzAqQ%s)_z|-WP?X=G@i|$4%8o*Xi>S3s zq7SCMS5+BwY_mW4dyy}R)#omY_5Yf* zY$x^LuY}(dIxci?(cu#)Z&X#;c{Gf*ylC;ncF}BPf^k-k^T8YGhv?LUbfB1;J92R@ zD8+UwUbX_qUv4bjk{DYsIeq@O&t=YU+^IVn+Pf3)tQfjt7xEV^fuC~b()CgKw&KLw z!K*JCw{EY-z0iKaKMQGw@Cl+95?hhR3vk|h!?xS^xiR^Y?_(bx!|!@7x=#7Nto+te z6)Ix?lV_CYGpFGFL^l40bHjI(^|k(xGfzIakD89?eb?{Ka`i> z@5i_WOSrZ_8|`}<#?m&zBNwNAFK@9j$XBqi`c1||NydX1cl-fkT#kW^d6{E{e~@t# zw_(4Ebuf8P)?~fkh;CEH zw9NShN|sl&fBoef7`JjQ=fOlhztDH#on_9H`Ld>!J!Ec_{*Z-j*KgrAg@56)6)9V1 zM}4m07TVf7YK+2OVqNUuI=5`ub7J+by9WEfmLrTmxb!GON6d!E>q8l9aoM&T2Jg-2 ze33U_A+GD0h6%?XOD5Z##c0rrb`oCn0m_M86(mKiOR^~_J~e-&G&$Th4x>M_=$U|hr0 z3B@_S1t*;nKw86%=6uVHD2;+1PCzMuqYL0!fJm9aly>2AgM)oX5zQWdM9 zOqFUn>;->G<*FEC>Y{@>Oc!suLisQNWh)qQO#MLGQX0*zTcfL2Pnb3_r~HtIeXuCw zqWAqrx`ZD%=l6)6ukbf&Xi5527ES0!uC2fF<~qP^oEL-#R5nb~txY%o!scIIqN<5m zBL6ygjG_l37fU}8y^z?yHC}*rlIPT&gl80sFMU6;C*0p1qyNrpp_2DU zxfJ*}Wb7ar*Aw{;bL$SsL7uQ%FRn-1e-JmG{HbEcNcIIm+{}GS19tAs(>t{Q?hnV>jOsh8|&;q`8l_RPg??`x~6dS z?XSK^`hjaen38b?f9WgY{~|o1^Z}6#*{(7Vd~*DtFVPn`?h`ntJ=b`B;rV{dmK|NW zAA#`wJMKMF__uO&CcZzx+J7j%C;by2uSyM?qxa0M$j)(@pE6P#pMeT{8UI4tUHgW> zookdU)u@vK`)zk#qLOi4lw_QbQf0~^YRp(%`Sl6zzarl0NH>1}6Qd_jL50djsKJ;% z)0Z4hS3i(gW5U;Xj!hi@_KX=-xO`O=6C(hI1KWWbe&SlwEzm|DUWB?LPrPRzxWPV<`AcA#(K5)V_M&e0nao>a%O$;1 z_FQAy>>D}uMD_Zn3VWgd1%&pubU<_O5H%0vEl?O9v5V1(wv;^i3Lpnl%)wha+Ap?0 ziOE&6d?m)zU6Ta=1nzZG$(Z{X)TxUFtJiXTzh;ami4&FaB+tf9nTC7?3R0)r4J&uv zPO~4(U2{g!H{U6%)%bUgn218#3*0Io5_7}(4h>T+MZ{N>8*+}SZN_x+N3Ps?(7IQD-Po_& z+5t7qnquX-dx}q%v95SN#*FDTc(iKMQIjUCxL^`5>~o~c4x@a4PnDZ5i@`bE(> z$oxNO#UX{iNAxTdEL#OGqn6-1>hz{Dc8Go-C{?Md!Z!!orAi$$@;b(F@FtH^r8??y zEn^U4vlXOHP_WmM@Bv~g(&ho;FEKkx{@a+Q_x-F(3D)U%;q^q1Av&IK$@7_Uot*G| zgSTEo9`adJHgCteOV=p_7eJ4hThNm+=ziiHEi#@_kfsI}mN{i35+QPZ?BG+lFm^ya>+`W7l8?heRaP4bU zOe{9@Pd@0P+}_<#b`2lkr1pSplt z==LGLxYh>-%v_0|sIL@xzf|RF=*)d6a^)`w|B-P@m$CHJJv9f254?uI@B-ooE%K1` z0qTX`Ql8qFUVQ$|`-puOj{D1`X>VknlKD#HIqBc67^6^Rp{Sj=sO$L=yB|J*Wyj8F z%ys^Z6|3s74_>lU^^xs&Un%SdG8Ra`*lBF5e2kqp zQqlT{e-nEH>Dq66UheT0#Q2;~6EKV^Wh<$(DQN~sz|L4d32=bL+9GR6DsKd&PN_8718UNA`%2unX z#JU zw_H{D|3Ep=e!xf_{-)fI!O=HZ(fcfvi5sGjT^vO%o?`Oc#?ArF%$-#1 z!uj*>z)*5_4nswb!{pBzI^Rq=qGai^iZ9rB@UgBR2>lMFGSw_DgA;AQePteazX+CCO(#J8o-1knU&8N+AD3p!k@>$a}@|0mjFXSCF7k$Y))?#eDwq1L|q?tAO zoJTr+pgs){yNK|Dq9c$#z&01%aPQQ{=D&Ge_z&{;&t*Lm$NYQoanb53vYf4p2g2fJ zqGG)!s9e7Z0-|G(i#&eB&kq&;4JZ$YzNawlp?3X7EBreQh()>Drn<535;YMu8a9Q{ z{u@Ns)CPtB#EFcDX@CaA#hpM_%4HdOuap@R7tb+T zQ0W?qmMDkdg}W5~ksHq-d)|Ty|16yUjjTGMrF-ub`1_5UO`BFu^cXoy#R|(t`__Q* z2`I+U*lRC8MKi{06Pvfzrji$?57c)YA2JV!Zbfv&Z7Y4fbspds2a29y@BreN7hO2;SkF_hy-=KkvDHgIM^d(K( zv_~^5YuqEvPb>CqYQII$)>MuBLaSEJiuV&b5E;3iwzV+D7dp9h!z1bmQhWYm*LNK` z#`TE#U}op4@b_U%nbI{HD*SU4EC#pfyQmAy3DN&(v|r2XGR9jy#_ksUWk0LF(UaBq zUVQvEX79hK>|6Tnnw$J*IqHjqE=apK zl&=6w#u;)M5(ncJ9TooK3*fWr5-K;fg%{)cX}D{7KkkQHgz|^r?+`eQdVoM>7w$h{ zj-ml#7ZQ0|YhRJ6n%Oxk*-`i=(GQD_n8$yU2F&_5ahG@VFAK}L&HB725At68c11ST z>Zj%6=MXY}Dz@Huq{e<`@;MT3S)=_H^oh?&exX3&qHr8MMd9Cf-d@U0WpuQ^95FJb zkC4#($8Wfw;{Wq>937_ zEpN)bQ#%F?M{vSs6`MCGb~$XZaChaQz5e4W|6w1BM3KdPHxpGZ-FYz0v z%q2A7U*iKf_G=S!HRsE?*L2ikqmZOwFS+=>`*Z&W!9OeYdmdBPCBa|he&Jz+2ax#d zLjNT;wKext9Ld52c_1q6;WqsX9h%IE`ZDiAOP3XNBsu75=V$2XT%}TpL=N zGL^MsclZpC(~W;=Yq7IPtb`9VK=i^zJ~s{EJB8oBD6y`{^Cyw@gcfP^Nb3WFfA1k9 zFpYbz=PXz>3HB0u{~PLt#doV9?WBUeB+>tN9XSn6ow}own+NrJZ~wLQ;MF_Y2>!ja z|MYofHz-`WmJa_)qzS#paBoQJ|MZi@<`%hl@bZI-9!^-h4<)(Rg2(^^C(WbGmpIl7 za;_X0zXT4niwNJOwY7{rkp~5Tj>%V~0qNSmIU{|9ItD-L8lH>HDP`%?{bIyg#;aBz z$%X!R>mLS3#-eWKIUouCfosnw9~sdX)NIi~hktqM*X_Aih4{yvx$_9JFT|aHBfa2z zGnnr3{*0zg2k84n9iRKGy{Zp1^cVmePyZy?YyH5Q`x4mo4p%MC_TQ`C!{`QDpjF=D>o#zvOfNuqvxOk_c0N>u;--JI{b@PtcK#H%c(t^ z4eb>!*VKLjspSUt88Ec>Kt`@z=R5ytvb1dv69;d2EjmFtxUfO|r7z?!R!YfAi;mt< ze#3&lB*8x?dB>4Ub}0PwQYR!f6RoYaT=c@?YbiEZ(&#H`VVd6Gh--7c_qo{gh2Ix` zQ!7u)HT*SxP_E0I^doJ+)h*j4!Cv}7YsRs!Yu+N!7i<&vA^0%==cRrqggj%ad+ zx&Kb`{df$W8`_`e-@0~%bxfX)-h71o)HyheT1IH4h z&%u73oCnNI-@SHW6I0;dg#MTp68 z+-c6*?Z`!+@DkPQA$0Z{gZTG|nxO0f>#zQyY=%1gxem&U`?L%8a}M89z5&|U)6Zr8 z7g2Xgll@Q5&sU}k26=C4ajFFWDvWcLoCo}#KJooFo=<^)9b0Gm z@dYaSui?;CS15LhBoH)b6GPa z_MmNdUaNQk8veuqi%&mN_(#lI!*vl!d2RhZAac3HA{Jhae4yBcQtaQJUi>-sFU!~$ zx}qO{Prp1l7W^%{2P4^YX}oOZ5BTo;iz{)g|qk9z!AuvC%=+Gp^ zG|ct$N@mRwvt+qi%P%^A_n3wBfvtrj4_^PHbd=A3EUlpr!N=}N&+o?`zQ(7dhTJQs z0M~lyk9+;&f?VG^Zq;sO`1%bm*6(e_V|EAHiRId`0e)-|}*<6aNl<+_mHC&AI2|oCU~B zTV~**1E@tEz<^PclkDB6zmMPNI-b0I8|++r=*rRd4a{)_o$ z^kS~v$Fbj#K0SYGF{x6U_qDRs+|bFBKEBhamoBb&Q?YAnejXa`T3)+NXSMc6)&aRO zmRENA2X3HW_dgx~$)7(=dZ3?{>^lRW(4mRfKH#C@@^9wJ`-9B!W4T9=hPi(3%5|fS zXz#8_+n3a)VF`n|}dznXM z{3kCX`MLDDQq}4pavS}i>BB8DNQvro4UT>31NG@sA2>ReHZJZ*(aDQzw=3%Ad~NR6 z@?#I*kj{RolsZj6N_zjflB`vaT)N#L{^8s=ssz`n)u$i0;4k{WyhTbXKS5bbEchmG zfBkbAtEBk~f9V6%1IT)z4A}(v%r)`p5;~IHuS35q?O60evc5<5X39^$*8=oixAIH$ z@zchvCn&RP}l!j{EJBklK38^Rc02}En@Q}H0A!LvfjDp{5`3Te{gA^S_YI3UV=bKitw6#aC(bEj#FLOa&>WW%B_fYt|a!>Pm9XwFOx+8s+@}vuY zk=4aFNARaS@Qj$cu?5o=@1OU!WDJsL+-FYt7o~-{{%-~7-`8Wpa<1LnN!wm#uIss* zD*m$GuT#(7*l_I^(x=QgdiN>L5lczi&Rj!-#^y@q>A*g`b?a6P9vp$TwryZs-3T2W zXfGNu0*4PD#@qKGO&}K3*L?1DmhY4#?|sO9jbE`a7MJAj%{xH)q39g7`_q$Kcj$~R zp(9hpzh+ZwSaxtx_%~@w-%-Dzu=Ea7F=O=G+|aSme_4y5#g_d9{QbEW=WmHcsqfEg zs4V?!B`9;W>K%co9XHgPPJMcx{MdG2Ecb$R$JphoQMr0eT;!ON6ghqha?mDJtC^Kr z|7G5+DXLT~gFfBtFk@67Y+X1EaYKFJ?b-&VN)$m?H&;A<{Fr;qztg=#dza)X_N~s~ zwVTi0;rXlASh8dZ`u7h;ODjv5nbt#3Z%_J^Pr-u+4^)46$BrR=ME1lsl(hY@wY;%q zOW5@aP8ENLzF}x!*+$`CrYhI|j$R1I5p$KVyndS-I+ise5;s7`{wI!qSFs;yIzK~Y z7`n#2A5fJ(J~I9bSFE1u*q6RwPdUN7Lnk=;_@YnfU>v-DAExFOC{eyLhArRD_((;O zGsjPe@OQ+$BeQV-=q%hjJVTcr@cfqp)8XT0gF*!h;L5e@YQOHwwAFkxk#=8BNBV_* z>cG!GqpDG5G;UB6p?*$SHgym-FBp#K(C+Bmt~mlQ` z@-f*@D_iVQc?vUD!LXUAZtajJ{w~~q)WpJyu^NmpYvp>Bu22cK-F)H4b&svNN338! zYqSq;?3suwJICVEwisO4G71Nm55~osq zgBNTd_SxZL*BIwFN8{AGC>&cAjw8!MuzqT96fcw;`xf@&c_@za`f2`tVM{ccH>`nS z?2lLd@Y=5NxVU``b}bmB`sCGJ<8WR2=dKAbs#pq> zCQej+VEu*-*mURwqz@TNGnTEPk3(ftZ8#@!_>Ne5gU#M(l zwYE-cb3?hrS`aM$0saN3`%`Eu0UC(LuBHf(E6AsKbF2M>l{47 zqcNZM&W1J)s9M()xpU>f=9z&wv~(c$EeOD_*}m8|qbDMJTPvP$_AthIoY|At{jh&Q zAPz4J!L*@W(a5YmZohnoTNy~daLzWAOik-!XkaJRx6W)BhLdZCsD2pkV}r(Z%j4*Z zK{&QD94FQc#_9D#v3>?)hp^uF?%h*!ft^Dq+<5-R;JdBp6Vt}CKV)9l$6Wt-{EpK~ zf7-;hlj8fd@n7AdeUf;H`fV@ALaRk)(~l?p3H1MK`i{K)U_(ro{zt2a=zhiKBmOC&v`1$)|-QJ&d^|;OYNIL)UiL&cQtv-qZ z^!3okT>p65;hSpROAf{z@Se3pH~uZW2dUVhT6-tYMeinlVA|N1JfryY12VA>{UK|4 z<^7@qN?uNC=b{Jtp5uDJ)~l)FFYzAo(AL+JYu#o4a!Jh@3#)>0Z3G4eqI%_0*gD-y z;k#_KJr)eL!R&A=bhIe1@b~Xr1G6Knuwa-imdA9&x`}QIj~v;v;rNZa_=R)8FZ9*> zlyr;qVAZyrs8XRM_RRI?d%dx7vIo|%uSuUXt5F=^fA@_lSI*_S{4V!(w>LwdK7I=S zY15`5ID81?efnu++*CAZWsfPx9#fx}BUSvxAN1?5zft^@HufcV95IJv6rmg=04z_JV(jid9tntA`ICB5Rf( zv3i^fmPEJ5oCs@7?cW#^{kdKh-@)P>7XuSR)=>#DI%STeFb=0&weLU;?*tdLLH;%0=Ht3EkDs*S3r+}Epf z12{W%Quy!Qy&LsR8>a9$#`-#r~pW;uO(QfhcmN>NX&g8!R`sL~$D;a=eJ2~dsxvcrh%Qz1UY10zi z=bXHYX7(;huBp!00P`?{d`_#d- zflVP81h|^Qin96DKi*_GCHE&haPby$=gf{3F`X3cnK{S;@%_wLUR^XWVqCv0-yzng zCMNjTMSO5W%nGx_Lee5j(@K~$N!)F~SOasAk8Ah!{S0@VxuRm8BwYL(m^9c_Z8Yu}Sdn9=}1w#nNwc?O1qz;r;Znmt)3^5g!2QnCa60$$gyvgGC1<&r1x7 zQAo`=`a@oG%TTE@$lFJsJ@7*s4%2s6W4 zATH1pqrIzPuxlvD}$I`#>Ax|;s;ouaDhCSwqy-1Q-An5QsF`c z5$)Ryg8ig`2CA%K?&Z;}dQKF|%~&=rB{95PMfQUlm>gt=>7f=VQZNsWA3rYe!^6Xa z>vjvl#H=x6O^#9ifV~HV!`jVPt@9hX>8x7otB-f`gXwZuQKL3%8* zn&|A3y#QFy)$lZM^H}4V`sb z{d^g9Y3Gu7o-9+@w*LP6Z!|Y=0&BCfn9OlEhB!xcEsM}jh0(u5ehli&wIswMws%cT z3Z$QD)e>l8(E+RI4IMJ?)=Mq3+kvw_kH=3#s<*`v^!dpWOJ68Lo2cuUB}wq_I$;%rCrsY6%^d)CG%@2ZG$D~myn^iO6# zl)f;`qXOwd4RmZ?289b1z>_CWl(u2PvR234CxG=@i0Jq^C`-I$jjtqG=U2I2 zqcriaP`d#-M$SlrzuUOw$}e1NZ!MQKpu&3@!e7?wu|CB!g1_9$=k|*KkGxZK0HRmY z%1bRT#5f)fjPa>I?&a9b$p@VS!;@ey<9{jrTz_Qz$9%Clo7`7baDU!Mt`67?mx^`8gYHQ$NDCqbr8QPEq)sy!!&DAN-waIz#)tmiC;# zi-3rcD91RDvfjXJP!ziO_gAsEqNmKmfT6J%I$~u0t4dG86?i;pgmW<+09w12L z0j#X7lubduTtmkN>HFgpGd~Ibvi2hfW4BG&cOeD-p^SBrk3Rn=7`Iu{o{JCR84!vR z#64@}tdvbMBY)nU%zr?w8r3ji z!UUWXXai2&|68T-(UVcKQZ=;k3PvgVI>}5?baeR|e?B#y`)zWwX zdAIOqhSr?u_XAn8$6?J17kd6gvXyZjhC&)&o4 z<2SM9z(s60at#S<_Q2J#v8$&Be@>JK@yb7nQ0SGd65J zlrC36`RHnWRQjxY?|#^N@-~j&{TuT(9Ds$5y~0%DJBrS)-|Q`j-FpjDPX3W34O((Q z#fPcEHQ$r=UR3i!ORfiO)~$aM{GCTF;5Yn7{uiEK{Jm2f`{DyYUCxtF#Gj$dzIma= z992GDdTUPgao3LXQ2*O~`o<*q*RpYfXVkb9*h^o?!M#yRRX4$C#<@B2%d6BA@7;dp zJ~p4Yjl+zWeT3)fO$)aiC7rLTa9(`)yuw|umvP^;wH?gNt#RPmpQ*m@=j+cfWb7=o zU@Ruzfx|Fq{(8jDT7mwQTf$?evhGVUZP|7#+~#V5;423 zC&5}D`}pI(!>-R@wMMQR*LUd40M=c4qD8NWB>0OBI1kqeOMA=uAK}ZixWIC!)M#s9VWEZ6fIGjYqK&d8d9}(eOR>X2#HHnuUQLJtJ^@Wb(GjVvM+_K zZI^hUvWBZ5*YVb|Y>T$O;qZ-Hfzdm!ss1p0(>ateHsLzR1_pgW>$kQ~G5>cSHd}|k z*k(lDGK9N!P4?I#hQ~e?f6g5Nl>c98?~`~_Y1xCalkf3cdPs-Atn)M`O-qiwoDb*z z7sa_(N5731R6NLi^ld+I?d4}lJI_5qea;azO&ep?=^II~7yPXpoKc5ymv>$G9S5$z z#Qe?2;T1Fll^Ksu{Oc=Kucc^%Bl-7$=Bp$Wy;LmQcUClB@YYYgB1TZYGr_po8_?OK z4~lR<2=QI((5)B7t=NQJcb}yT-_-hnJU?&WNridWfDkll-A>_Nk$p@2Tk_Gjj|#T` z@V4Yhx`vNV(FclGsD^Hn*CnC4au02}vpyAnd5-vB7CNH6H!T_< zecFG;F&+LjTXjMo+OQ3cf7>4Zu=EL6_gd0#teuNDc3*n&kJ6?Sk6>KKj4_A1Fz(Eo zB)IRs_Z;2(M<|m_>P29(}BKH`f-d^H) z3C*|d8G;_8W}{F1Dg@8jiUIR?FcuzT_!KJv&*76WWbrn|>udYOXk%Y;i3j4l;6M`m zt6O$Z@t$PAAPtYSa_WWNl7@%>6Z}OE84*xQ>OfihS zksu(y;m`pd%R6%48YMd)?o0sdFVGZ4n2d1!O1fa zHXYs3xJ6sk;2w~cww*C>;)*SD(3wj$XbBcN^Dk zSaj&zM=?)M(>Gsxgn{Fyai7X&Dvpc9Gmv$e5{D%>v1skpAKgaGg#XmF2w(QI;_ZZ| z*XQxI<2qLDQJa3f3s2owJfg(@)W=)@c*aq#6Xkje{kh*`(pu%)A#1WU96pv)_bd3< z{wL%A!yIrzVohu1Sz1~eqyc#g(PlAuwZe4B;m67!Y~{@tDezxLc}sL1a*u|8Q^tpO z@$QFxS2>SoAZH%AyLFf`O2>~1QoNgBFMPK64vTJY-Nn0~jQgZD+fkcq5QJCjHYi%*wuF4T6=SdE<~-Ga z@e3-_FE3wF%7lz@kf#XuB`#l$dlu9{i7Itas;UWPJ5!W2GEwnf#Frx%<6~5)Q4gLG zV^FiHRjSxapBTJ&D~eaCnK=G8TxKlU2B=h-`-zaIr#FfJ4)r&G{!{GbdH&_$yIzSt zQ7hjN4G{frrxCLhuI}+`RLpNfbN`$ZcNhyXPZIp2j{U*5ErP|{PEmG$nXVK$eh&8- zFv5KD+qD}uW6Zpon7V22C*v;szj1>`D8TRV8XSv>n~uUIU@&SnZlTsRMb&yPW9|v(+&3KdJ^R6?Tc0%X z4`l5B3iTKxfj%E*jEyUMU~4j%*8U&M#coU4@AT)uU*17EV2|*8`sE4@(AIkk9Vl9& z6xxQ4SGdaB@9K>$4UT`&{fHln{`i;U28NDnA8F)vIg&*xQRVjaq?yA162V5c-V9=@?Nif#OT<~t!cPwgH z*mIm0QTv_LvS^0})O{*if9wvH(x*#OBl>h2iUX`a7f2i!W3GvJ7&49V0CRj)&ez98 zJN~fW=AzGqKWTt7{mYbJxK@VnBJ3AB{~+-bY0TI@f~D|*0& zWi2XHtEJP$9io2EQ2P1$U#L#JYM3^H#K)bx_xPvbzGw~iB&nrr&`y2BVD0Rw^fKjZ zHAd&qnTXo_$ROs!cl?1~aXZk=nejcje@r&|WSF$(KAmCXF=X{I#s7)kHwWdR;iTmn z_L9rqPeYe;@7FZu|4sqn^!@5Y-~Sx+YqCl*=Cro|SWer3%>4sCBmUX+?EzZ(Vt>@u ziK#U}&21H~zDo|$rcv1-{^IW~>l%{dFSbHiLo#RG0qXf*rJ9_2_$fP>>Bxs!*mt2kR#S~zbC-VH!{Mw!%w@d$#P5cQjXTG80miAX2rKu#%BEw8 z=Wl4&1+87Zl3+jk;C1Eqms;!xtxs4o{;9-7sB2R1ldu7~8 zywR|iT;?UgU*-Yn3q=^iPjHpEPq`@%7#jZz&OcJR|K#|~xvUMfpsgh}`_8aQbCvwm zXV{nw<1RFRSi%ye!#Ap9N|`Pfsx@f~_vn=vzT>Gu+=pzq54(WzD9Sj#`HPiBd)d2Y z{b_?To+j&w9w0mIzzK}=ra$(BC(TDy(gnk*qe-Yyv$-b_J@RxPN!W(Qjd<{o$zen6*UEypskom$B|UbuVgJIdOlgoRGQPf65xf zpLnXneKLKx!x^tv6((;g zyja0fmC!9_O{%yHjW5UfKQsN3%t*sWZNGwO?&&VJyY~J=P>=I`k@D2@(vL&dW7VeY z-M8M{4f9`_*A{yEa8GzlLlWlEo8+yFJMH5-tOan09TzYmDt-*?}qzD_{J zhh_|keU~09UyMatj;L7L!)GtaF!n=dEmrF-vt-MGw*F%b%4;&-g%(tA+D_?Nij!}t z)1p1h+PkSZ)ju(Yf5m!D(bUD4dlXJZ(4zeqedssDkYDLDX9rsL3R8QViY`F*Gp-vEx&PqzD|)o+OZ z_v8crFra3Ubf3*0fIz zT6b2(wUO91l7@3$bD)e{jr&x{+#vfpHFxoW@1*6hq8-4*qD_)HUz`8?O_+uFdmoe1tg1+((Ut`k$Z{%2=aS6zX1JyDgL{n_ZNLmA?{@` ze%o<__*Y_E$a;{}<^b6nwh;A)4e6ibK>ytI%<drsPB5cjApzrx(L zA2H0XVr|Kq-0ajRG-MpJzy*7gU_OT5(u@07)aH5|(aYE59%W+h5#6rF^9fHVw*5Hj zZZ*vHa|;JIbfk=_KlW#xxQmLl>Y*%sk3Yw_*Km;7ZN$G;Mlj9Dy?l2u(FYo`LuvDX zMbAMnw(5XD^6UDvUpp56Hi-kB8vb%!_Kd4qw_b*9^4o9yuJi&$xc{4#B9twS}TAA&dS7Gub>XyaURiDhE#6+-2u@NbMy>VkoYTG{SR3X z&P3iswwi)#d)Ga8U#OggimHtHOWB;~E3_TL7>-J3%$8}|mChT#XV&NF_t8fms|p?Z zll(89DoOhlv7R&bwdL=&NbFlV3 z`oQG#@wBu3>1=u$UxUdn{J*y77I;wD_NOJ?oUz~K_n1wGRZ6mlw$}eRBQOb_1LzFD zx10QiXUktmRP_<>MNwT%fj{_K0UEo*xBeDj zzhfLsKK79J@(o?G)oR~k43wokj*a>b=l-92>aI$cJ-5n8{&I|%39jF;`5^occeXzr zO;7#)J*_Q#aV7xvonW7+t+GRQ34U-goo6EbE?39^uvZ-GM9VQyrS}*<^VJ^S<@s*E z=PGO7f~pYdTOgYiI*%*vOVsYj>$Pmf%d5WYPWkJ1V7}SdCG5K}OX1S0|H{L5`(0>& zTVqeIv?nO0xBzH1+htPEu|C}{s<6D?* zy$Xuf_cJrxHyO5q=J}t=FMV%b zr%QhuuN{cTh!HzJo!Qw%!9L58K9@AG^GZdwUmeMgPv&`R*HYE%lh22mDmtw*tnJSY zsNI}>26{{D{yUEJr!#4&@5@o&zaigKpDfFS^`1RDbi9*2M^2TE*179wKZT5eI$gY! zEA@}ie_$VaAxCXDd}fh94DyX@)XicX6s^&Qb#fhPFN(3h<@Y#}PnIU$54M_Z#x8WK z-=+Uc)orDUx>pad?`Q41MEjefkk9Ufd>(^k+Xd%6?DOFFkMO_OnZC7`mh$UKW9qG~ zeI{6M&~tx&C#=;iEy*}aRH>yzCJetn+33ta>HiAdN2s#38aRag*}rHymhKBr>bQpt zFKLDxDkI72K-Z1yn}Zg2EOopfq@fF^aPc z%hYMibO1a-G}4@OeC}O3{>9_-F%kw7`yI(&AuPJ2!0yqY(t`r_Sz&koHPzAHtSoX z&9}Wez&;rBkE8Fv-w*7C>yu10r<;e$U$ueiIC!+({_o>EgZ5D5)8#vj1}~w$Pr!a> zjaWDSh8!P0wX3)8#x^OFj{f0v-(!BjUhN{~t6A;)Wn|-3l%B!wi}>EQ*JeR|4!)b{ zy%}!-|4&4>;q%|=H2}R6kNW;1*>UN6zMLr|d_=$pkfZNl{RsQEpUB@mY(>_dy%Avl zV^0S581`6nnz3K2_jIx&PNaIRI%bpRmu}KUB_mlt zwfMvxfBlcSAbFbfDkI7B93B6TwvnxLp!L9;HTeti0`Eu$)YBf;jnAp?Q_#4%OZ&Xp z?~|?X1{4o}F*t+L-g?lOM}-O%Xg@_%@^|=ifPIg3elFTqU8Mt^7fW)TYF);Yo++8~ zqWxw@tjllcW(#{XR%x^6QG*t2V%?0_(tUyId)V>AAJ>?*`_1|u{9i@#o=u~M(J;n4aa}h@^dB*;}LQ}XKWb2U&bMR z3Figd+GE>gxG(ukD$255@O?39F7Hq0NK1bkuh7loj49}HCv81Wd&`pXK0Lv)GuHvD zkCHuBLgh7MrCHzin7DxDTA8zC)#qMk+9@b$A>)UwML=y#yc^Das9~$mjq%j?n~i(? zf>JiTi!+1BPSEPuw3RpLCiI}0$iIf=o*m2!;M;pG`;F~1ekR+<&ZHkU_PP8XOKXYD zg zyfxjc@qNf$X@gOTiX15!q$bCHo8MZ_8`G3bnZJp^M8;`)rC-tskhqAGYN@uQ9ufnZ4{ErE_|Vh3{=V;)~+ZY`3C_XnAcjrBaZ&c!JgC7XJ~NLMb;yM$qRmGlhB@* z?4-|w4iM`$^nLb{RaLP%t!?%_&LB-k`bO~L73ut(jCqTyX{Vk#-TxzYT%xr}3e}!` zvOt#OvtXl2OtyPix7w411g5?r^$z)K`b0Efy|wWl{c(C#~eX@3|p`ymSceC{M**#`C+XNm_yQM%*=bA^OmonhU~uS zum3$~Z)2UI@5Up_h4xp$uZ}f8TlVT zp5(G0Xbg<9?0@p>qx*CSiSGt?o{*_zq4Q#JzGPvt-{`mDERO@M|8VXw^sEKSR#w@H zxT-0~A3KeE%=fVqB;mCjWH|6;AD;VN+8^hDgU%BTh+-TAp# z6`1!R^M{=X-auxBGb-$@U7c+sj0NbPum=f#Ibm-UiN-{V zjM-GS!lhK^{KZrTI!`Mp&F%23lbHNeWTm+TI*YKrj&rsxY^5hzG2}sRG6rm=3(0u2 zHGG!FCVwg+y^g{dgUteLZpxG?qu|2~a?#+TKOxypA(1zq&GXYw2ft+hOTFMn7WRJ8xrtCfk7d zVWIy~F7YQy`_)2Shhl7(FZo;iI0x7ID4@DP&P(!s(117rr3=kxn5SVo2K%9}i56gg z3~aW@UY~tyhgO>t5B`>H1H7Qa0B?jjIHc_p9jP1mGw=kU1;M>OaOC@;l?L-L)|;67 zV7nFC>kpG{z(bM=%_iN?A<_@{(Rg@I_wT6vU(-6_71@)$BK`48oE=4D^cDG|ct!sI z-q6_ji}b{Au?K)`4*$VkBh&>UM~)o-hd^bVZvRn9oH1HRb zKYxBc(-`O1U_3%b09pW^J+%5Hw7!EiD$Z$wemo>}ZjRQ2`uMf11LTT1fkGAvxhWK) zP=Z2P3Y92Sr%;bVQwpsqbfnOOf)@oJ3Iiz&pwOR!HwDD^rO<~$ZwfsrAbocV-6(XW z;6*a2VKnZh(imJqV{s#4;so)iPvN_S!Y7gye2NIP0K6UO8t7X{?*VaS3=rl%Nxz51 zD~*jP6kKR5?4-KH*b9I;F070JYi%8t<7gZtrExF=;{ZC%(6X_1rq99KL8eS+ zu40Y#e^l0oROSVAok8oa2{g|grRxXcao&d3{s3F$56gWWP}!(m6Ht4Or|>uI)NSoc zvsLflj)$ESa7{e&8|vHl#K#;X-lYwdGo!V7P<$eK-i5}6ANEnh)+xBl{AX_c_R7-vME~U{zq$L^!Z;z%U98Nt57hr ztFTvA%jfidEGqjH3h#(V_%F2dQdS8(HW9#;uF#I*F?|%ro7(L^*G^m6y@(q!J&N>&a^4znSsi^2YywIXKe(P zR@r*rnX-k)wA7v$3;8H`LbgC{dyo1Nb}*l4&Ui=d{Dv_19{1F*|IqIjNJD8aQ8+|t zC(*l*d87-?`qTHyQu*%C-10ZhT(s4HL5&a1I)$ZgQTzT#Au;-uLVVnZrShRH^!Ede zNl%PPk`=xU3BD$*)%ja_{QK&paS)B@P=6W+f5BciIAtH-+S`A7ht)QraS)rvz&HwT zu|GFB#z9!E>$h^*9+xx@5)*GQ3;Za_nEnci4Cz}t+ivHu-ZnH2Vi4cjp2ouOq$~Xk z`W$EX+lBQyek-42PxRELgvu(Hc%^w0l4O^Z)x_Dtc~tf57k@6&Pf=M+GeJ4UbWQ&#gxf=-ya7^ta``vAC$9<-M`^ zc(~sV0gc`S6{o&E!)r8`vct0@?R>8 z{<6GxvFLC6_vYir#S9JrgynPd@#mxL-J6YP8jqIu<^#mX93M2E&7Tv#q$&7n|K99< zd-$T~X7#0eKjZi4evrkz@s{vs_BpyYukQaWn(Kc)W}hPhzw#I)~?$_)E!-irkJE$jv8jCPJE*|WmF z7|VLl-c5eqxV4wsckbP{5QH{xtJHwolI#YQ*kz$lZ22mJy(Y zH>Bfm$_HFn#CbqvqP~ky_Ep!R?+wd+VGsMXezS^w)QNOaJpN`bKl%;(LBKv}XaA84 zxh>%@6>STBz<;Opi0c0NK`mAnY#*2s;tw8TCzS@=<*g`&p#^ zVd-iO`>^qlc|RcRV?R5_UIOxS`EBkGI`6abzytLi&F8=}=6{S6=?BmR@MG5W>Vzlm z2lQ1~=QqiQ=o9h9yJ*~eA5k!FuO3v__(bnc5Fht%M8Q7lfqg>2f3-SozTrQJbHC8f ztT_sDn_@o(+8DNBgcpvppu;eL-^#zY~_mnUn>ziRyBS${8e^M01}x&1vKwVxv_TyV{Y`c&EN1JO!#lszCX7q`bXLs ze3q>95oe7D!2UB}AN>JeR`PCGxx&8-mG3H*FK$@GXK0m)_5$>y{<$yw-IwSe?CZxk zfvhjE*as~~9?|Fw&0Qy7eZzk+;eYy)L)@;As~E=y%^Ms~HdYFI_pH$X*pTBn`UU%W zjqiWS-&p$+_ODU7Qim4K!t!@$eV3K^pfj}Z@?B{6izC>_UQm1G!uY~CD^;%MH|z%! z_OS-od**KyD|S3?TT6Wl=5@3={L|Umr(3*uaZUpn0`vy<`Z1)RV~o>SKS1S<9hNZ} z+GV5m_?~!yp|p?UK6s;$?g!*AYeeI`kF}$$zLF^I9~Jz~K({0AfcK?BNG ztjBE&J)m)1fM+Bu@w0%=G=&dK^bK^jg$fli%fpyoNdG2#zdvcsi+xIDAQJX;^kJb) z)HWHYPY;vtqGyoxgjAovw=Kp5){)pND*Pku+u(TcjgDd;^a}IK4?p~gbDx05T8;z43lC+W zJ}pIb;1tpIm%-g%vUbFnLEbV};P(Y_7*FtZ=IEHfd9HXbPr*{(upi9W$Naxx|9#d2 zf>v9T6G0Ck{btK90DH+rJ_{QyNBcG?=`erMod1l*@qQ{_n(%Z_=h8_mQo+8*?qNkA9>%SMOWtW4VHV zKpKn#XLvkE#stO!ji2{ajvF*yTVdRWH>`!_@=|;LKzg9gw1&GubM!xs^ogW_&REuo zM4R>TfpZOn9|+8x06K#4lrCeAZ`cnZ>|_3)y7&OMGs%c;(r55dO1J|i9-`bN8z-KvR`M@dUe&SOM zKJ0}bfFE;%!zpy{@Jjl!-=bce7b zwl{vPeGB{sed{C1_3jX#znZ?8CA{M*yp^BY{71rdbI1n>zb~xGhCrJ!XJAaghD`i5 zR;W-xpDSQfZmo|UiNhR%yua=Dv5j3@n07Y4w0j+HYfC+#Wj~3{CgIZNIg#+WLAp53 z59I z%tY5W5Z>>JJP2b!&LgQ_y}C~KK@$SQ`^h*F8-j29eVoHTWck1x*7TNGm;0S3UMQcj z%T$Rn)%XlxU|9m*wbyT$17ytyY+GVq?(q!yf{zosGthP737*inxr{w3v_>pPX}+!L z!->rj^%eZh>_oqU2b8l%paX{e2ja2r3#xsQ(3dgOzwP%C+&XfNpHSPIXG>oK8_L*m zHJvK@3V4U@59GGOv)h`lg*M{~*(}~g9!484!9K3w4McxSGyrGnou>8T7v3L z{!;C_k5rMPX!1B<5jr|+G+6YlpbgfZTZ_ZFei##2@7eMhg7=Vy!dnWNA}T&2a=obE za9%Cz^kg3|_&f0#gJ;2I^C3C~wCw(+RjR&PV_`9}H-J_|ZMS?}Wt`M;xkGk|NX>!rV7<013DwsJ9()0`>NHsGIR0E8 z>t8z^7&>7!_eF|S85H*Qbn~9f_tJKjx>(WEIBSzFeTu#U{y{Iq)&pxl86UaQ85!y>P3B&lEP!)%xKr^;^LxXuxyQ* z{2Q_(K`!9%APKxu4kjyVtF!Tq;-PabQcPB;(0=g|8UoivXVh z#%HpYL>llL!e7+(8l^@Zx}hRR(=d;5B(~;~7v$St^Ln6Cjpp2@d1BC<_|xZtUdZ|Z^aFex)&oM5LBEApw7d$9r|)i!Xt;?8^`U-& zj(;1*1?CQ+<jpyd`bLI1n2i=})8V7kYlg8}|vJZ!NKpd)NodIHK=&iU^pANOhn-nT&CP zJ`%ky&eSyWk-f)HRJ-pz;<*K~A4~pG#)#!rY{HN~_z27AaxXq*5~t0m#vc7$`JTA1 zV#e2eiGueu(h#gYz$-xihhx9Ozijk9v8TT z?T>N(#ygfiJjJe~VuO|{S(*%-7v==x9zocE;$4KFi2jQ0m-u-^dRu4kif<=+_Y#Gu z;Tbm(q3qNL?i6rbCgVwXH^H0ec-r(IsE)n-TXi2Zp8FlN!5WS&Pdb^= z`){c!XP>Ls3AHm5a~3bH7XJ1~xix5|<5kx9-x0k;TUzP|MaGvhLl(8};v^CN8_Fjk=0?&0Pb*I zf8h$_ze?k#JZ?ZA1it}YW#1USfnX_f!W=GZ{^E6-G4`?UZ#`iVr^mPxnYH8r`61>q z+((P?vzoAekBXN_8}rZ$eoZ<-*!`g$lBCbd*a!Yy>bK!I#E;qft6FyTg=#-~Cj0!8 zHcXsJJ6jFyYwSPthsgM5{&GrfyY*US&QpN%gWVs}V!bc<$W{6kGC^6BL62j^zWj!+ z5Mzqo{Y0Tyc*Re6E;qG%LJD^%DA0518}vWo5X8uea!!vb8F+ql5aGQA^9zH z$xl%UR>PmxBAJAPNkf7;yI`5!V@d~fne3wnfnP2)!mnYUW)fBaE- zOb zLajDk)!v8ysgjH4Wdig6rl&Sie7)uuCJANc>}?q4cubWKlaYad)h zqzfXi%U!mLj(_iUzv?)TMDzQMqc?OKP@_X{eg^(OL$M0{+tUAs$oS$WOssa_|A%S7 z=oLHkvqOPh*!^Ps?$WO!<8!Ok zNXP%4KR&90r8Rvc(8mEF*I9L#AAGcR!IJhhD_G6kGa3m&?)>4!FrP({gr#&CLAYaAH965KkNhl+i$;Q zJYY`Rbl{H&PXll^650fza)*9q_|NCA*=U>QSJiL91|9$40iYMiQt-2^N6rHm{ELha zYbwY9g$LMpkKf=6gr}o=4E$2_ zVnqKgwQW%fu)APx_yLy#y*N?H>|;r&KMT`H;O9R{e8Q#Pn|d;G!# zZCSYCoZeOy+VnERe-%&7ZW3*VP{O0W8U8^7=AOE%QfAg{`oZrD_T?(Nn|@Ol>-Yy9 zn6&XA=Ls4u_L$RmoKQInYkU4Jb-XgigU=BA5*dHE!d}Q!&VT5dJ)&bVwChK03R(XH zv@v*aX?t0tBOamKl7lLN$tNW4J({=Fu@BjQewR|54)S-55zu)VBS??_#T+kld;1w% zRP|nC_#Lqo#kvu5(vKup7_jMAGyL~kukB@*^7QhZt$I(KD`SIXO(oU7hyMhK{}L5F zIQ_uUO9I3wcxT|fO0AYERT@o(E#GzI3LmBLr8mTP)fd}`IB(5qy=}_1=xT=l*1k*i zXaA%2jzaVJf^!f3(E#6HPOG0xenO1vGxV>>@Cvw=H^V<@K(X@L9uSce)Nb2dja{YL zhJhxCt$@%H@iT<da0wxqD1f|>7DS6A21Es2z{g>2XOQ1=JnTTkjxQOV-!baQW0lurpDy+SVt-z$VOulo zWA1lt)J`u`+N^Z`2Vp&DA&o}`PrvZ+#Q?F7`M*XhZT$lOH`eC)S<8>ox?}m6_{pgFo zbo^uA57zVOZ_NAhUL@1pfq1O-WGzse{7?Y@afydUe$Xkw&JVva-wfMx$qfIn^^a!Q zyE1O|PPO#cYn&eMRBzeAF7|={_C6ZFQKNnb9gC0!bm%c!rOl8{&kuR#AX^6DeZ!vp z>h9~e>f!s34uwD7{i}|UJ=AdDNpwz8Mm^8SBu}qgvxUOE931$zm&c5yhxKolYS7jU z|C3KY)&2asHrI51xl6iP#sAVP&vjXVrEFJZe4aym8UOV=_Te#5pIIB+2w%mB7idWFqVB*g~_z7HLXDU1ccu>KKO4gjJWyp@_Ts+9J1b9~GsFLsP22dH=mlfMO{hAqJgTA* z){!3A7kRL2lP)~>Hyyp0pMlUQYi z5cVwp$M+TITEhB9|!ohLHB zCC9F-T5UC34t%%I#QCajhtFdMc0jTm7`{-eb-=?F`gXRHhF58lDj z&IdTR7B*l1QwCxFM`N*ONG0y{!+ckW+JL-zbxbf&>`Y1p1|__#`ngOeg*A|fIUCp z`Hi7am3sWFLg$gnXWtXnxghPh?I&&0fXVwVkiEvw{2t~9U{++hMcgW@MqR!2Z-U-0 z{q=@w+ZLfhf+d||C8It%S#Mlw+%T@LgQ+anqBy5IsU8udfp^uHc zl~LXY8y`Fe{)MmWyY39*AL~Or8?$1yKm4amukqpu(qvMDkN>GUtvbeO!7CbNyY+mg z>-bNTOVj0xjKS92U#N;ELr9Z3n~wE-CChOB{iZI|<18_c`^85utK3DkISXYkM0=IC z9Xj&GFmH|4oW1qCs#d!W*9T>Uz5u#MBbEd6d)zE-O@=(Hb{=Skeb9k2O|`Xpsb;;@ zki#!k@@$%oo=3YLHt`Q0p!G1#hcNmUIK^53-@ux`aW`|j0$_jH@tgXZ9y9`LfR$(N z>E{nXUu2y73G6~2iHeG#e75~`p)1R%`f_XAkdcjO`EH$51)id%v;i- zKi>YMMopTcV#kTgb^L*NF8D4B%$i{FMoqthw(mS^lRx}78l=f=a+Iyf_-`{)v(G{L zflIg9#6D<(_rgtFKH&kxekXU)QXF5YbuSe^nI_*DyLuPP0+BDmn3caU{>QJ~%lEeA zfIYMUP^97fw?J{H>wk@tbm7hbCGgc;l?LJ!(b z+r;?KL-HxSU!!GfGyJ>Pt6eWEmb0{Vr|bh1oqywz3v~QwCg*occS zhxkd7aa_fg-BtCD+Pc3?jRw3&K=xl(Zq%Cbk99!WEE>OKOAZJcfO%flqR=-OX#ma? zNBtDo)~LYU1gK z{_yX$_7J7{j`!?ryz+>#zVFe$3j5BH=llcbb^Ncpa9>~NgLd^DG+6!d_Jexz4`Dx$ z0c-V5dk?6lZQ80hroC+VZa^U$>92>38K*Aae;6#ZVDE`DDq|LHuU_QHQ5DYfa8wq6 z^U$QMF(_D9n=ie&QZvFXYU z#{b?2PgEST4?;TV`G9)_$o?V|-9S9BFLK&^WCtYJ$F&skv`B+cu|pq!*vGoRSS@Y5 zRBqf#$N8*XC-@z#D@EStyKc8m6A~p(qBiW_7ii22t+;gm4>}vSs9r{tGiAywG|sC~ zJGP~E9HiQ{o1p5}^-^WaHX-|kvZOPOt)~Yj#!Z_Mur7=IuVB_ffgleHs7VmudPp*!)7*kI=fGCg)8_ zGJL^4uJuPR;`hY|r|586j9+1f|4fBTb2{i0hVJ^!AO0ttdZ5Ht{k(l=b*wk*so4y8 zG;goK3$*AnnC;9_N7(ROdGJvENtg*r*m>kvl_aUg$Ht14fX=S!pvH{Zr)}ohMb@y}W}|7NJMDE?l!t#g?;m<0n>QXKuAi14Q>e_OPFA{F~-~;6G_5 z%_a!#S+Q9MyZHB6wuAc-^v<|f)U|Fa#($H6nyvtIKjeUkQ)u+1MW10h50I~9SUdnuoy%f;hpp8X;TcK`4 zwe;{A?hCxvc;rIHzei8aX1*e=cY*yKx9_RwradmL=k74We-@XD{66f8pua&rkn@i? z_`nSRKb!ng+LHUVo4TCeO_ec=j(yDcd5V?dc-Zk4aMf@I-liA%AAbD)-ymTPG~?8@ z8$2JO+*z|0RWoN^2oUqOXhN%2V`)y(=H5Z0#{{`tLeGA?b5|8N?Xv`LjkAw;oqBDP z4nUWPx(wNI!9V_&?BQp^{{#Oivlrwzypx=K_XWiN%d;HCT=^Zt89u56*&Aa1AGS%` zX9Qp2un(@>xDBTT=4B1gcAz%C;Ag@T^OpCpB|+IR--C~paSa` z!BVu7(Z)yke9&s}=F+yZernvMms)fBir)V{R~={k%btI)$Xj;kCbNj2 zAfZ~j!Z)no?<_3@TI1o^&;&mbi3O~U$yDt@7zDw6F%zb6UKi`;x&=q%2Vgn#>>}P z9)~{YxZ;Qz{>9!GGzWRhda&_``DXYZvimmYhcRZk&kxwgUO&T9-H+dC@@sPk)^X6&ppUV>AGGs^8U7p5I7J?*bLO`?_ls};blGz1 z*ar>hFj%u0#k)nQ+_(RHuAUOl9$ex2ACL9AFI@P0kns;1kSUYKPcK-xI@ro-tnZy? zFIDXZ4Y}NCm!|E!&1is}{{>y2(e@X=doSP5?N@^K4_LchcU}Vs*2k^dt>Yhi15tiUzW|mrK-QE|dqbOcd3EqH&CDW=L%&cN}8)zg0o`#}j-Xh+7(+Iczs`mG8w{ueB`N%E>FJVvjQT+)c!;Ql>k z@>E?0UcO2rT_#Z4U0X*r8!_7-{`(02Gmy^GH2-HWqqTjO(%Ll!?cFi@29-6xwQG8l zk5BYnp3;?d{P&)($q2LNqw|*_10elB(qON!$N)x|bctBQTVmdLFZyJ>4_zRx*gtCA zBP6;)dbT65SPGBtQGNTMJs@)zJwLFBIIPW#Ykp&!iml)Jb2rq{r|)%n`e-^cw*>jZ z!TTKs4OL~TYh$7SohfZO_qSW5(SNb|ft?ZFb8FSx4F4k!J*2&Snx0I~@iXEd{(Z3r z81F7Waf9c7pE)aZJKp+Dn~|LFpP=DB0GiRIcOR|XZe;&rlJ_rNdXL7Jw|+12_O6{f z&*Z%Fll@AN=u`mZHRiiw(-sw-Xgu03Z$VeJaPf4b%*?Ef}SmPL#-*|@nCCFZ2$OUD-!`K%+0i~-P9(6DS+o8eRNrF z3!JMU>#=}zE5frKeE3Ekd-g#kN|v14*L%w)C1-PV8a9IEE|}x{{BqqN{ws7F&hKFT zg}xHKpXcoDX80GozXU1M`kVJb2bzzW%lTxcbLGJEcN;xTmnn7X*-JemEQDm(ee5`Y zA8i5|KCoY z#m681yUy442c^tWNXLJ^Doy#_lC8W|o@&}!&+>c#YjZ#1=AV7QegrWF)8{Uv;~z4B z6m<45;wNr6Kr(=P{2TPZC<7?wUXA08=Y$CEp&+1y4(?ORA-7Lz2c^>n<(e9^mXHC9dv_@mA_|NBFo!^C@F7IiJ^u8TJ zI<%0)y)o}$9zgVf0gYGj6N7IcV2`@UuOZtK9sqg)8UK{7OL)Sw zvt_0FO-EaxzOoJyoj&Ga(eoQ|FTX{eihX~A|FYG!H6#3eK~D!9Apa<|53m3JyH@cZ z-E=Mp))=y9E2m++kA&vImA8!<>_(3nhHp3q#jv>p;kf7F>R( z(|~s4G+I))Y(>VuOBHQDNsn=}b^ME8pnzlmqW{MkFQ^w}TA0Jc{!e&c&;gubfix5z zQi$R#uEIlls#hh{5pqz-K2dKM7Z0tXO_;1*;JHPMRv@2hul1v8Q z5WgOl^MAJuXZbngG5Ch)6KeMLHJkfE1L7r1&1rFlpI~0D-s_L@GuW)jUJ=jkJwg~i zw&}{G*>hDw@o6rG*$Or zzWHKh2hDjz`yWsalpi#_UVmRR8sO1&Fh46=*Hg!TPB#sgRek2EUi18mr(AK$}2n8*OvT_C#NhQ>HKXElFG zR~`Sb13;eHN@})zqHpiI?v&NJzlP5=PA_{nL@wKB%_%ed58Qr@)BQwyz$cvk!yo?Z zcx!fGIFl0JuhX!RdPXw(=QNhTRCxH;J2h_Vbd@S~8ZHycWh(&pDD$Z+*S=VJt+i8M z)45D2w_EeB`d$G1MqJr819bdnETY*|)+V2zz<&vn10W5~1QdMRyEYr3^<&l|t~&k~ zU;n6Ko8}~(8QOR9Le4jTsd760K?hQ2($+25H*OrK@>}*9*X!)9uMP1G^g`s=qW1X719Xk@$L~~W z+egQL+MJqPc*f3?3TK4i+wk2k{6DVkea3OTEg7KrrXRXwyGoNqo4X(rlrb%KZr~p< zCnA(8Rf_YZ@7<#i?sWlFhs+eC z+(}28Xs&L$JItvss0Qhvmcvy7wnoa}8tzk0dTH{VJTB!N#y{vovUC|a zFR#UW82_RJ>^^oT<3ES#?6#5QYfN~6br)^R00rBrNJg;f);r}^yBX(iJZ~0bUSxjI zfe8;_*c;?W^$WM>h*Q~zpnt&Uz^)(llQ|dIvu58L5FHSEYWV&T`9H2j%9iK0#Xg{( zTQ1tffAPkhv~*M^vH3^&AnTKPU#@a~*Rb6eb^JpQ@FT5P@XqQj+pVvkLvij5Y3zlS z{Krn2s`p#%L30`Z@c$$G{(A0tq~pKCy2}dV0KPjHk&NEn+%L~P+V|vdxHs#<_^)Zw zH#F<%!}u3pp`Jr0>i7p8ko^G|o5ry#d)KQr@5K18*su-fZ)q3R1Z?UINkv6><{_~a4)=U{`9dD`c zGv34RAHI(uyhOa@Y5ZYd_Wa>&KhgJLpI_1p+Iq7U`O|y)&nL$N$?={BB803i-nK3= zG`{PsoN)m8F+{(?#-pk4dgZd}Dnvu4{W_UA^MAmn8l ztH0CrM{5`h%W+iZJt)7C?-%=L@H)n}!d#4W*!yFQxBM;g`Xnh+>G;PUfY@{n9=_Gc zcSV5s7hhkXw{pfuZnql#@Q<^@kzZok=PCHdIe&=DKznZP(%!-+A3o{fPof18H8@4* zoS}(7@4TdX&e_U& z6fIYU@$WfQ^N&!e;b(uKz`ytfH}dtL&yR|vR$Y9`_%H0H$rE4`WI3)OixS&+$i^^N zLO&*FP0+Y&9+t6gSvD#!@9BqZU*sC%Ujuf3mi0H-Kl?jt-I!MFsS_kgs^fp+8m*sV zk-kQJ`Woka`3-*`z%K%9ji86b?WAf>Ye#5+vYx<(@{_tO^-*kRfkgzNKy(3o_lRmFr?@?;l<};i}CfeJ9 z`QK~GBEBzKxt1CKU%yjdjyKA(#O^O!f#QsPtp9Trb>(!>dl|>H{1(6SMw=kai>M=o zn-qSq#93Ipr*g)kz`px|SI{!pIZ_V*uccOCy@s3dkC^r5LyA9OwpTV@>*p{sjurB;DY+{86ka0@iXc89jZdoQO z=M=OD{2ieVux$`Mr<}hcxR)#11n(f&N`tiEtB~gB*l`&D2Oqv>+gtGSeRf_85dW|V zkZ&Np@LT;hp7)3U%AE#t+ENYMD$iMK`FAY(=9Nd!)C0nQaECuA&xjgydMR`>7_N6; zzbBif3fwPI$zNlaZFkIR0NDXVj-l;mv~@ zXUv(;4F7}Yt>btjUn@8sv-W_F|K#a1b2>{oppRO z_Vs=OzQoUo@Is&gw&eX{3xs-NkCd@ZjKA?d@Mkxfk1k<;TveDvg?Po0jY7V;BEo_Z!XR3mD%@o;jBd z{QGdeSpOqk(&Q=Bjc0$WyML2^`*0fWQMto>AQ}W3>uA`2{xXf1Xxv9tede0cfWi&6 z{Q@;S`#6Pv*$ncSfSrTll8jZkj(m$Pnb2Qmz9!=*`rFH{#?d8W13D+s#FdvAPTo1(% zR65(-Qv2a?@F0-~nvS|0zx9J7G+< z@u72R2=m5}%Cw%arTe!Wa^IA44@;wc#-VJ8~n>FTaN$ zxUbumE!}=VU3u{_P~j@Q7g0k<(ZrzUd-2Izl_f_m?%$l18mWqXr}FOtWP583=K;l~ zCp|yLHqHRG$N#6xrhOBBL{^-CY=-^y*Iw)Al-ra28?hn2?jS>#H2}`VLYXMQN8lH= z1)%Roq41aj`z}Noq4&c*%51EQz2CzBLmnr7d;+4CCRwOzHf^Ir_{SOn<;s#bpK8)g zvmu9kS1@n9HW)IS)27dyRb77m@v94grFUWFy{*SDlHU+*ZCI-9Anwa#w2vbotXq3l zp=o<uEG9ag&1uBF{7z>&gU)N@M0N4m$1oi(QTdz>MAI&X@eowH+kcGaEBNPx z$++KHA9v|br)WTzi3|B%@cj1XfAJ0AIeeNw{C66u=_i2`dpNLsF17@)nHT(r1zP~Z z{f`vz|Qys z13xhJpl0s}-{5=x_{cQC*7{%e01sNY*&qIkSE|M3ur;3v2CQAhKb`o@lzH|`+X4{o zn*#grQGhaGj~{pylq(?nW@~jsoYWKcIGyJ0bGaC-p2j>1L*@W zUV2Ph=G6QT8W2Be3VwINp-TbcKU;onUy7yP5B9%u2E4O8F~+*=cLBCwPbWNp*a%X+ zI)wHbfW8SkUp~JFvN)7UbaDaN?%TpX;^7kweUz2XeHeB4p>8v5O$I1Bkls74IE;UH z+T)A-8+7WTrfoIRmssZf|IodDO6>iX{Blg4ee!Sk2}FO+1|m2^nfp-}*)x$YUvV{o`1J(jg9YEP+%q6+|JX@V zIE;Pp|K+<3;rtqP>87?^zshGFw%<$i+#~_Z!d~L+E zGrxu3mAMx9$DUf#8USZ#Mhi_EK;Ju!_QXCWprgLWpY`dbKs4%9 z0O9z9cdChJ&j8Z|^nP$W3k!5KJDmr}V~O;6&R`#Dhp*FoMx@S^RV_ZM+3-UjXo>&0 zq$9vLS`8R$hX3pZwRzg;^Vc};9r-P~G4R;Hn*^Z?^j){m!asehJn9Ku2JDfHb3g2I z9I21BG_vO*A=&a{{;xZ5Dz~R4O_B2yJo-;|3jcE6Z~cy)8ULA0`$pT(*uwblzVr}( z4`({>zx7fbfB3J}aDw7PEf7sO5rj~;btjcDNiucV?~UDjLYv-&=J!U-TE=}Fjr!JE z{4crml=tmnt{t^ZlP5seXNiCPEbua)^?$1_{KHn58of0ztSnqY%2S)*6^ij`tN7ULFeuu99Uy%Qee z?9t<2AAHcyWS@5Un$t8OYawk8Z)7?HuyEN*{_rn8C8Cl|$-1krm>Kf4`ty0IM?wStg!^K^KICuMmk2-pvhEMc+>bhoUk}*i`JR5oGyuE+z9)7- z(hfMg*NAOLepfQ-6zjC>!1!<3N5i{o075AvX1}u`}`jhBE^4 zh9nO_^@>m7Kdfc2=7-#0%48{{w=BD(_wb*F_G>qN6XU>q;|0wu#VJYR6_=ymX_-TZDW@*{w8f;0b&>wn*Mnr(T4)EW7C0W_y1t{Q1WJb<7`=N~dnf30 zFQ{}gB;Ws6b?QHy`@d?NZcgKW&dEFcuJGVq7Q*=7jC#TUHol8}%(yrDJhxW|d(XuONU2608UN4& z$Ua=*{o$`OARqrmp5OSpTE7Wg-s;U;G5$dV;wIGkO8B;4e!HX2k&RqX!f#}Ed-=tG zY=0Y+d~Ck!giYd?veyY~v<>AMHeIv-#yP)^&ilr6vEyrF1!I2nik)WoZ_!uNEf=oU zO67L1%i|1l7IB1?icL+2lf$i6)DPb=+i7Y^C)1~(QxL` zJ9Y5JV|DA*f9mP`PwM@D|EK7m+21F+|A!)9e)vCimwe!0&rM)#aKGkvY|=p|>GbRk z8V{eXuuGuupQT`NeXPvdb;b<;RT{VDXXV-tRF%4o($fXj7SG^w7r#MRlZy^;{OOk}T1-tYh%z*pyf#qym$QE| z=gF(%zvB>XO^AF~??0t3()=7yIDC_AgvsByN$0}+-NAzg?Lpq^bI<|!x(MidVd>cg zY8z+54We=IHU97|oqJ0B(en>e(gYh~m34$5!gw~glmha&zL0wY+<;G4Rw zZi*DaRzGPQ+1msAAFTfo3Rczhs3=3PZI=Uu{~lX3U$BW&q}1{6GfR`vCQg=u*6dif zTMw6>|Eq>gnXBTO&KN~G5WKy;b!>wVShsGS8aZ;L>fE`rs#~`%w^i@nz14&X6V#zY zht$W9ANBP3f%O9X&_hc)iL(mcezXvNe6F5Dzh0WwMV5I7 z=Y6~cnheibKLFqZvyQDGzVFRX)89USaIU=x8dJCjmgIw);*IZO=2JAg7fdc|4tRklUa3dQ&WxY z-&!r6)Jx47(^U=a-Bh)z@2=9M)b@u+di2}r)2DT8{6T!N<@cSsC;D~O$)M3Z(7nKg zYt&}1|24<`jT<-A{Q2|M(4j+BhYlThy!7bNLyaFlUTxgCQN4csTCcO^56DZgJ}@tj zf0`_`(AjHTIx`mS@4ax7)AK*h2LH*#*RT2Qp-uw^&em*H(vZ(6!M}%TA8Bye#fcw4 zqYscTfq#+xi|#Kl{!HxA0jVqWe!~C59svD@^Sd0yy0vsMX>S4Y*>LF^?*Rrs6`j_D zm!7^;*U5jigo}T^RgKzc{=bnvf9VQp?B>II|BIXg&oZY^p=OWntnQsyq~2Uzt6pDO zV+yP3ilE)&*}0ulRKJdORm`6?I|lF!zyeWj> zyAthUD`0&G-f8*r!T|AD5U4Vcu1)4n&?Bnc>cSoMfnV`n=zy$y0%a@n7K16L9QvClNw43-Kl(Tv< zS36REXVQqjlSs7A8b`kUKm)`lPsQ#df`tDh8MXdfaNvmYo~G#o-Kuz~8?FX8Eifer~h3Ts!KgC@Gp zl`B{3XFXty#EJE@s_#)+4er@gO&{4sEuP$0`3`EY`gEwPN*2mN>!crKyec<0H+A#o zO=~1Ty`uHNmFFLHn(Rm$T%)$L6}BGNx zypDhH0pdp{ptC+5#fA9#!5&$`y<9^H|LosiWREic1-JZP+Cyxex-B`t_)nj|1h=uH zzJ9IE{fJ9fNYj;0Si4bGYS@_b^d2={-6Z@YOqjon z&w*#TvZhgIw)(2aXBMhQr{=3aPR>&gj?Yo|f1OQXmb!NgVWug};%ux?1 zE#7%T?=GLxlklR+q?$Bo!Z>(F@-rz7)3N>xGr-~ORCcg{2R;(5N`R5;hbYTC< zbL!XI4|UoM+8Ed{c!FlbkFwV8+%Hi0_h{FH%iNUw2#E|J8J!J)JfZg!{vX%0`Lr>M zJ^M1Q1A7(Q7wGy0_i{DhKg9e1QT@)Kj$(TRx{Z6|dFO$xo3;1E4j6twG5^P){eJjf zpPko&H2>!``4oZwzvOhj81meB@PxWeW4hPi;re{+;!#f}Op!|Y?l`ViTzRVEna-6$ z`rL&5n+K*b*6$vjq5KX{Q@m| z@5pqf4WJ3XEFP$0MAv*u4IVsLVGaB{>CJ9YnZja#%znv|CA$4rY@%ONhPKz&Ij=6S zRh7WW@xr&TpUjs^qHT(uOPUH)@-D;?zdw=I|MWJ(eZO!YB#up{u z;;2W{9=-SqebBWR?^MH9n*A2~vgM$0szFa5RnV<6&;3g=|$jA7vU z24VdAp7HAHuCeOMFQe7voukyH9V6AH?IV>>=jv+ZBv1a0I6S-Z%NTWS_gH=h^Z_*E z4$+Slle?)O=-ZG7T)K2g{YgB)ZK44o9*D+(_Bk3*uB}_Q>bj0Pl}o7W2WHY5==0hK ze9hz23)CY7;&Utmo`G+Aa)#DMv@Y5(r=LofN;|g?bLEQ{FU)Abij}KWgO+WAL<=uI zd86}n=ts8(tpbIAoFRtyOIEJM_#eJ_Gv6o9klhUXnExww8_Cat$|DD)`wK<gKJI3?B_;o;i7mn+|n7_7roVv1eG-Lb1)?w=WmLclgrorm$Mjv%% z!$7rbK@akU{JqMaF_}6|_lUzYq&dH3sJgIiIMWHx1JDof3ynN9ez8=kQtBPev3K77 z_c;(PxD$u~J|eh+AC>3A#Y_4gtbU%g80T2e$l3>M9_ERNzX4wa+6I1ygXtc;&)p+4 zDBVnTpVmou_sX8hDpOj`Pe1DafB%P>U;F#?>66NsDU+JNerurXGJV-<4T~w#@U?_T z{XpS=z>=-}e$K)r8UNKya|7)Djl4g8*B`lv(?XwZZHzk_C$cu?G}zaN^Sj;|lC62#MF0t*)|6!`OX!GZ-UeWuKG{_=;wB7!Kfwv@ZWCU9!?A1-EwR@b1(XEtd9hH#;bw< z4@vwlLcbgRk3zSJI7iO*cBa0PR`O40I)@YfI~?WnZKdmF@+pS%98h+IL@Cp#=?5+{ z_Lp6HtRj)^ap&=Kl-FEszkt~F%pcX7a6gph@j)icdokXREbpNXE$yZbF7BfCFY2uJ zE$FD~R>-U8KgGMI+P|=qIzZ2m=J3+)>ge*G3iJSU9H2ytTi86u0zw85sKmP>>FLT{~B45CLwp{gx|K6MFIein~l`$QdtJwX@+S`bI z{0=4j4**u+qg%c&YxLmoZDg%oB>$);+y53bHwPL2upNvPIjWA$#B^r!g42GCeaQLS zkDSWS<{rPLGUY9(r;A5p9`iNkZOq|EX^scxf$6>TJ=N~H?bWW?ZPdA*5yO%8!JeMUO)`(zty8SYVB zXom+xi)7Eo;I6d@uVWbRz%X!rcGDo87dpAto9lCYwU_!Sl168%yXE0_)A%1Rg*0eO ztc|cPVm^py0%%C?EShcEoH=uJ{Nu;u$&<+zD~`JU{4cZm+oO-OSFPuMOi$~_{wogf z?@aj$2MYg?6JmYU%bRg)vTk} zbnFBFnQ2@kp|jqHuQvI6p|u;*x|ht(b2YFE++#imj(;J{@0ih2ZJX9iZJE+UZJN|b zZJgLpQ`Dj02fiCMB;b-p1x4P zT}X!el)oe4!rcel7tx|dCd>}fFmIxVpk3f)KogHFGx0)8x~gq6+v#mkEN@z_=K z4$?EUA=2U*85ZTo-@Nfcz-;Dolo9U z+MY_`t(tfgR!4|u$M^+if&1;#TQHu1_w~N@)Y@^i)#@=d)v8f7RFe3bP9OPIb;+Ps zjjo~AjHyLw>ZlFA^_fm=o!XRX2k5}Q1)hux;2}+NP1kws{6(k6xpB+~_wO;2HNMEb zWDcIQPp<99xJ8>BTB7kmSoiFi*MWGUb_#WxGq?`7C)yxW>IBUHpxy8cX^@)Z|UZNmjAN0Ycg=V`Em2`gNwjMRZKIZg5D2xh2OTeZ8d(pb7NOt z+k?*sFAwYj_v^;jWz4S_>7kYltD=?+siYPUu0V76XZb?$+{rj@>Cj4Q`S7Z0<;dzx z3qUi#FKnCEoUs8sfW8#WsnPs(J9o1T5OTmT9oo^EDQJ_9jY_l31N;ig3j6}gIv+&5 z4ccb&x*YdgL;E^!n?==G!S_r@3bj28hHNvc_RzJV-Gr~ zQ@8IqL>uR>)_DI6d5fwgH$SN<#~yH*VkbxxX#D4Ob^lzil<93^A8UZZwOeyq__Z>Q zYe#;IUmswuVZ(-IxHDofH1H3)LVr)um$C-{We|Uy!C~WMtxe>7gJg6jkRx-vtu#%i zuID)ba=&4#cKO3T@ZZc^)7Qd31nP->yZ!rXx`*a93iBAncmds09PdF)aYCSUIXX^8*_vHefvL zoYjiw#4Kqw-NcR~$JJeuYlnqUt%hbhGsv?N%P=AHz&s9Gh_w$eje2N&P#vako$G&= zpZrL=Q1C^oN7vN(AfyEi#kOF{A8_f?ONV*5Pv@~XMf2V*Tyxs5%=;t z6!Fh{fQ@oU$o|m|A+Zkt^g#T9WhQ@Mwy9pP zHMM(TdR9!$>Qhq9?N^%V0C1}{_=Xkqud!gQ-e@KgU(OP;J zYa+-;KnFkvT??k?cb6_*YJ|TUHA=I;YtgX_=|!nd0feJhZ?k<0_UleR`GoO5@4{c4 zFZS^{ig(L&Vgrmi)gQ4aK>XJlG>g9%7#SJ(PkaM=%LVJ!t`#a&;B=JsSIpBP6)6A3 zC=cX*QV#Jw7E*S=vhS&2t(JkpKm3HIrhS|~vSKyQ9eOkve_6Jo^^ZNgPdshKo#nH9<)o5{d2~Hp+1PM+EEH3{52&w2J{XLoGpJu`FWNNrj% zDwLK)SD}SNLTKK=vNWe(5Cs%ez8%VKQ!6jc>0gHC4=hKEhE}AdBdXF$UL$~SY?{$y5-XUZo^V*B`Rba7PqP!C)P9(rT z&H&J-AyumrY>*6z`Q3n+vEtdv;cW%>vHo9i<0o;?nK)pQ4@e#0SJ^fUXMB3|zPty$ z0Myq}9UvJC3%-V+YbWuhcl9Fv#TlTC{eik*FM#8;^ixhH*_*XcXfDg4*$ zK0@?=<{Y_burch5Y=LwG6)3haY|@#@!!(u>8mpCv;V8b1_D-=Eu0j zI9IS=nHCHxFK|DjS4o=My*O1Yp`05auP*iT(6k;UXlCyKn#+7(;ou5lod7IgKY;xM z#y{rKzO5^Y`i3`;q%T-!J-JYyp(91TI);}O>vgdP; zXq%Ga9rQugjHv{F1phA0xW|^WP+OyT)!gWyL?%0jN*9Oj(?s|6w~U^2nOgCicMtI?Yw|4xzi}2oB`h zKTn=KHn9&3dc*$*e*|vwD2w{I9I*8Q04FlMpl$C`ct;~8`Q^q&4SrArZt z>rjBg0-w+SeH-Vbi5&}4e79mWqgN@x0~QSp;r&82Arsih>jqdv?s3TAM(`zCnJO*bkewf4*SbKuIguZwsEw z#eQTl?@Mxr{BOXV)k@wvI{$Lsm)EX~-WAVc4l#>===)>;iTbPcZ6)5cSNKxEvrKz{ zQ%@1+p9keY=C7`U^aTjtz-HTM_Bna}*Ja6Ghwwjz;|Jy{9w_>yHOFk!Vjus*pU0$a zM}&Rf##>Ja`$S7W68sQD=L~;SpmjAcnNsQ;P%SgpOt-7-TwMl z`i%WmI79b;`48_M+)KBg{!DN2o&-LQR!?a1T=t<}1A!0n`UZW0^-gR@We<-&Ema^5 zP42>a70d&{Cuj93P4fnnrA54F!g@ijg|H52TVGi>V2r8%U=tv9VN_c%MXZUhG@8}ir!_cNe_1?h$)HpNxE%ypsrYKK=D){iyV?#B@knqF=M-k!X!bej2Vl_7OAg^5 zvB6QNY;&h){eyR1VA|)~aId&ud-GcbLzx z?#J988+{;6nWL>3y`CKttZ^b_NM7SDr_1z;TvIRMtqtHw2; zEEyEJU;M(AboYn9=^p#ua~m-)x#t9_`A_|(k)m&!)F>)=pr|+3AM-x;G{8Rk0T>6b zo6tTVrDpzz{;XI$4UKD?NA%C+&V^|j?~w!#VBU#7gzQwDov@5BYeuCX4jecjyI)-4 zGf{pUT(LJU5fDgzMT$}}|5EhPx4#R1dG_N6Vm}H!y=B+GPk{e9CqEN;KO zFW)ZmC=ycBF8-w-AV+Kf5FZ%z9IA@+g)1|54*P_q35) z$grXY^M5gA{%@O`=lpCmyjf-%5s{fjwN&Q+NtXE^YhcI={Hj{#Qfz`2WZ>TbD~dhmN#oK%P%qaGY`ZX2>c9qhU{Zfr$W5m$t(C^U;$+v z+qQaIiiylB_+TvWkEV1j!s~#Nf@cVM2-W~&!l+4g#U}&oKh-vX_z3WOi|+!;kTDaj z*>#9M{N{JTBhTOYl8#-wBYqEv884ox(zJa7>|-4;;~4AY^ZU6A6?Gc_qt_~X3qQ6? zk?VY;>%h$yM0sX)DiAs#^CGX9jtNDK1q92#_WwMtG0%^0pO@#m++xndJc#)a^Wx0jy#MF*f}Haf4-ciFqKb}qmTcK* zV9W%b`~NNI+QT2LzvVrzT-n$8U8XBvJfkIB_E498Lnxq3IcwX|e!tuqXlYDMEB?hE zh}Sj1KiWEu_dBCnW}}|<(o>bBTx|OzWIt4S=tn@mtnm>Wxa6m zB2?L)|G~$Y{3hz_>%8V{*R412mwp%g@}qD5McG-e8_z)3U%mFn@2LlGiL`vhN;-}I z37Zcod9W{9BizgTLv~&l&q9~cXpU3gm%ino`=iE}t>02s$ZhV-J?828zasipyEl=s zKpolVsN1dgTlxUVoQK!VjB7JAbngu%UMQbO%sG6?F8;TE@{HiuSG0+Bf^&0h0Q^=H zc-x&jcf?>^v0{ao1Cd_dKRYcPR!ywqpUeMC2@Fi>T2$m=9AwZ1Mgc|C)54fghEGv;{kKkS7@@mdILpfRm;i9G?<2jCNs69^dq zum58@mZ7)aRCw_4;lrN;pMOE`ALC7353Ktq%vwaFrp#u)J}s%opb>P3`R1f~%f)kk zrOGD4KJY*J-D^sE$snikFJl4cE*@kT|HI$?P?TrJPlnW`!Ct}MnV*^;V2-l%O=B4N(r(S!S@0XW%0M??9Ia^SeC)kMOhjbe%A3 zF@5yxZwf50$l;KWBu;oI&F~ftXjw|M3u^$h8$2MM`5$nOvk>qCT(K^|yszeka|V>B z`W5rC?<2*|3o%pF>;C`#_rF%VLs=i}%iza55Bq29&~pG?XM_7zg_%~I(*MMhU|@h>EjQ3Xo)K`S7|5Db*kuT z{3Cz|!6h0lxhZU;GW0 z{h$Bz8y-XdruXlC%Q7WJ2N>;C6YMb$9Xcd@*2?_}(o?g(e}$6SsB=R;1L#(U_qFAj zA5@?PEZ?5dw;YY?5J-_V{m9=h6R)G6+cXv`R)S7_^jMTTd*xQD99oAieg1>^A9e8* zoxAgt{Ta4`+W}yDI!#mV+zS(pAIi@x0%P(xyvK1&bD^8&7_x*(YNT@Ukc;OTNN(@sEALtCqbb&h?DOy_Df$4wZ4E%wQkSNZA|rO=0dhkFNibKmX0f zcffi@T}P=4=q{gtQl=14H^LPD8w{BuzKitRXK(IDy7ci;zH$w^{mtK2y7J&>YS+D= z;TjliA3b_B&7M75=tH7EwDiX7uZr_B?S1^8HG2-4zHB3X`0d}c_2>mETd^87ZqZI) zANM>#w-~nzH|(a`jhgXU=Ns1gX33tDIt(06+b@4imw);n#joAYXMM`PBsioBo&M-6 zQ6IeDq*WBnUbR*HKXL8~LOh++mEcI+! zjLI{=ko^Lg7h z7yFAZeNCMvDCa`i^A@1c$SCr!P?hqR2%!AMOA%xOpu9!>DQ}SyLM9Ab5D?^rxe68$ zBz=Hq%Ef0rjBi^c=6~jGsJ{{RabK-NqsEk(-*Vl}Wn}ac{v**I84Fm}5%Nmut0X8W z$Yy(O^@U86=b*vi?d;+o=lz-T6cGI!x8;zjxgYp%(4nVzCo-x#-TM0PM6`O>dx_>q zITqy={1^7wh>3>17<3j?-^9v85PPCVUDh8s{65`cUUT&N=hP^&9l;0b#Ca>lJ>1ul znlCx~d*OyX3EBbt*K86&v$yT13*Y@MaDS2GZFU~2=+~ibEj#p}>yLl8@w(dx0K7Jx7tU(j@iSb-_0`I1M`wc3bCp`rh%Sx34vQt2zOq4Hs8tXbA&tRV$ zA0KbkvjF}fYlY2Kdd+?x&tuKM_v|gDKFlL;t7!3NMb9R4&b+kllRqeK*F}mLFrI>I zg;SoQr3gNHkcTUjF>7{?8?u}ho&H$llg@7B4|4oP9r)Dgl zfWOh=98YZTxew_k^Bzyo+ILS;89oP*bop|&URjJ!hJO|z| zcKV`3yioGTn@|6uej{Sp7f%)%H)Aocz5k@;+YgJjU`)Kp=kBsR?G@?JSBZ^{r3Vil z0P-S!`u_d~? z@&1g~0R7gV5%q#TpPbjUS83;i`NIt6)z3(MNN(r1z^1o|%M<>Z`PVt>^Mdr zeD#<0nMCC?Kf2*0?L2vd*BXtj?ZaMv_{>H0KFi1?=H>O+=igCbmiMFoFvqUhbJA3~ zbJy+?X8{51BX#3}bAq=WxcI5ylhDsae%PObXI7|Gm0Gv!K%>XS(eRit)U9W4ifGZ2 zf`ZElT^&5DM|gMovdy}D`S6wdf)5oc=AY=@ta<&YW}BYWd)7vQ?*$j0(u!NZ+NJ47 z?@*braBF^r1ItqU?$Zfmg{!VU66x?GWrTZqzuS`i;#r*M>y3T6#sz_bx_E8T1jm7su{nh*kzopJT()8MF_MY^81iu}&@IU&!&&B!)@xr#=`OY-{yT{HF z-v;tR@c7f8d`s6EkB$;BtVf5he9rUe5(;b3j4@D%0)i{h&wy!n_AQeD!mi`D@F5_x(>O zqJ1~9mMO!sg4wHg(6z^Z(A7u(q@~*qQ{Ukes4DwXmN8T@X6zASv3Ke-l=n#&b$R1E zr*5*WB+9C@f&8HBSF&;~YCU2aP1^T?U7YLjg3c3{Sm*rI9BZOm>>S$s$qxyz4;{dD zH@_BbfL@BkxzROh#%A#>&Wp6;9y)Y#pQzQR(_+f#e;J2|`@mzdzxDVEmsb}3t{0CO zZ9y8=HQF{i@;~CHARl|Se=XvciRa+A%V@6GyD#JUWn&u=lrxQO0I#u3z*V~V#c#Cr z*fmm==C|AP46Y@XM-d}nDI>pG@qzqDuz_7+hCqUkuFi>dLh{*5)ie{gy6d_k6X z9l!aN!@N)O$`j1f`tqJ23$HW4>qbtVM`!LlOOW55b05>fP5Wu;(oHmm`Qo54@zi7R z7>ee2D>GMar;P{Sr(-w1Oz@7(AJ?_-o)v2v?3K|*oZD1x(w+t^+DVHpf9n+H!3UOH z{f??fb{2Ue-)c?U(6aaMCBnVb|6O_Ifq3?EK;zvTpFZ0gzn%b0%ISntaS-}dgq zHvsC)v|A6Sp1Q~7{ewxw1XtSvpuW&ufV>0sf)0-(m{&h5ZGR!xw>AF<)M+lt>cwks zJ)9fegAYK|QQ8PMZQYr!e)(65qmv(fOZA(z6>SmiVY%M^kH0j;J$S&W$G?cPK0KeF zW!p!tJ)&#ec0n%iK=8V$OE$5NzyPYk{J1v9%ZzN_o%--PpzDAbYTm9Zh1P3A<@mg% ze3ekDP&JGyvLDS#HR@31n)Spx)i~C2^*W6i>*3;k>|=80^Ru?^4aiYjb|tLU_2zL$ z?oB=NF%>9TM&u2Dez5J6I5)aFBL54Io+0wq#sI>3ANUw3JUrZK{Htt#R(J~QM)~sC z5&l4caV?*cemh*z2bTP?$*9?e_=iu>x6&%Mwr0=#!4L2(#(vr?*$K8q_??x{xvxC> zD@D=bEk`JOE=B(OsHca1^%TKgGfS>~LZ(-tK?`azbP^3+d4T4fe{6~`3|g|A(qzaa z>Ia|CGY(v|i+%6}83QCs-hy`L|2n-D|8ZLXUsBH(XWUxsIP$-Y4a9vMZ-i4%-RCle zPjIb20MuXlB*Xs3S-&7SL%`UHn6TIo|2-_R!SeIDt%pXyBM>)R=wW`Ijzh)ek_~T7oPkGwTHu@(-YL)x)SZ>*=)Y*`Iohilq^=7t_@B zJ88k5qk`t|I!tqS9;7)t4)9szKAN@lU7ERh4^7{+i>7UShbFGvOoLg+x5J>})Sz_~ zRS2z3g;;0Fo;(5azjAe(Q^cS+8nXOdntS$P0-n&U|2W|r0(CE6C)_IAH=6U+_aP6& zdpV1gvWx%f9S4hN)iFSER*d&?R<1oia|HW%R_e%bejc~|(ee>4GYixIAnS*1mbUIv zrvYmL^}62dZ|sZEPp#wE7~;QrXQh85`bIm1|H!_>Mc!dOMvA=4H}53=?-vtK7a#oT zak|81I7@ra-=jjs6umv1)%6|Eb>_2s_1<3Z_c^wWsSsLAv@-|WzwbVKSGNvl?)*dx zHXfu7y@yfpfHGEDBEE&1vgOW06>HU_c5ELscF}5D^WG&oeD51N_w65a{@I@%r&CXU zrCm2ZqZMo~iSof?N{3Vyu^ZL(l)tm*D@3)U`ce7%k)j>oQ|*SwnVR#}_?IyN^O>;! z35ihj7p4DSwagFtfLhEr`akluw6BvEpWgVN%f5{Awda6nhxAJVJr7&Dk4F4J>NQ8r z*4l_D@>1`yBNe7Dr_(g$h|Z@|OhJdJv!A zMY7(1^vs2{WdBJz!aDcfz_>GIoMk?@>HG~Ew|EV;V*7x=id6_c-(`E`wPeNW6uo4N zkmsxMZSVKU6+1;RAuYNU!ec->#u+d`7LT6{|@!NFh`csOYxPaws zO%m`84IZ2gsIdedJMJcGl-gX z>Ou8dw54jSpIDanJbF9;V;8=Fu?OfgeW|H^z~K4oMPC-LR^RUWA2vcL1N(H4l=b~NjF8&AYRO~Qv zvtG8|9^dHRiudn|I;Ccx;A*~7pp0^EId$nyI{z83VI8IW{EmkxdeTCnqkys0v_m(m z&aU1%Uwd!*#@%Av0oPpzjHYU}8(Veuksrn+_SBdwvFAQ|KY7jV?|%4D$OC|FU>Y)o zoNW6CT|(5~oiuL4e!KgDKGT+nGE3Kvu#Nx2pIKu~!j?hm#;g73x`KVY>uuW~J)Xk- ze+J(`&tDPb0T8rGcYgA(Sd%I?ZX?bF z>vkGQ;k{$TSwQE-e@OmU-arP zZ5x4SJ)9felk!9C%_Zj5*BYINi2iLmbeu!@?=y9gc&}WujtTPZzxBMhpM%GgJMr&q zUZ?N;PPM~Zi#kEhH-5=F)7al}kiu6 zTN+ixEBC*t-Yc``W8ptfPGwA6d)8J=6u~`V03QGll)d@1786WOz>!aD}}*WMa<9)3onEzNWor z_WzTQe-`I`I5Uto2eO{HA}&|4a@D9_=RwqS#yXmE;FevC>){^vpbG$*4)Pr^bqO`? z+F$%`8y)Ks{;_62p4FlT+Qfg|zKS10-~!L1lCp^uLsqHXS`T0Zt|UVSkA-m!AFDA^g{9uIS1^Z&>Cfua(>OR>nxT z!F1@_QidNUBh+4wVe5z`AtB zh2IR={-JKR$Um?=^^2KGJ5PUV#k}M-$Q$os?Y@%Fx3qhD_5YWC{4dSew1-NTeQqzG zhIOx-cI-}z_nmP2c^uB%ux|)&--YV6j=GMb)Udyqg0jJ)N=tYbYzk`T*r&%|CDFAw%rLZhykk^;EEUNo)O}cLV=D zM?Zg-0E1@dXGfpI**ne!Gh|l$7t4Nvehy@GnYcbUpON?FHD8H98nR-a(7jjV)*Zj0 z{|6sHC@X~ZhvwpJ5ST0 zt6%Y1;vc4H#AHP_0r}jdRan2w21U7uMH~V%QzeL#KGk+HGQv zPtU%&WxZ@&-)6n-90iIHVsn?R+f3N&Htjc-I*jLYy!l%xX5$HpKlGu)wms1ML%9Vx zp4(=Y=V9*06>|@42lQT|DSv#F2XvA6%DK zT0ADj|6Grq=ok3N(`yH015yqE-=6BZUhlWG|ENP%oUu1v3(bE@*ITiH-VeTKlWGz9j z*Bv-^CXZFeE>L(>S4yk$*@ONq%({bN5$&l4$5nk@;~z-YU+;={GqOH*k>JWgt`A=w zy=HANW$y!ff8>eyp36_(7T5>=XY4#F?!(WQ7SE3U4}W1OqsOvCy7(Ww?K0tP8P6DD zK@T5}-jnC!Tux9@;9XlL*CPUL#u4jHb(Qj z-u+dV?u+*zJCJ#4uLHJU5YJ@HoR!b*et)hDsMZT~^Y5kGPFQ92y+%(=g#A@V&r{I= zMb{C1T(WEhnzmu5+xPZn@edi`6!sGc-8$4qk{{!%V|1Jqd#lggrJVT-iuB?Ys?)-Y zPYJ#_W}p5-;2Cm!_}+nze#h8F)Fx&owT+oYZAZ;wTmI?PDmq@Ahuz7_kx|G;TWMIGR~&r!Tzs1y7E>9q;_AgK>vPbW~?NkBJW zi_tdI){3@6mZyhvqkHi8jru`vSevK%f9Vro?mG65=VATd{g)pRd~l*(tyqU$J?|f7 zJ=@0ZJBzg|(sSka6S^>Faq8~pj(zbR4V*kn)C2Vaz0KznI9r>y|3U)HZD6}XKg(QM zpky$O-g27P_1mb;$Qj}+FQ{f?%FA{Pkon8J+;M#?bs7p{*-fn$9jH~mk+ks86@m;9 zdAA%i+9mu$KLGg{`Tl`S5b4l)$N8Kd2HbHkGE%W&;If`11=fwqx~Zkyl{-mWi+mn_#3hVLNjptn1yMw)>QLuRxhfiSktAA3jHrethcfeey%j3BA^&+TXu({ZpY+n}*lbs4FN3+vIm0J(V`H zKP=3#YT9!BD=NzJPCS?D?KF&WC1!|xS9?vBIvwRIT$)N%Z$cHDcBjhCdr{REeW`k@ z0aT;yU-*lQw1#crSh z%g@q;{a@1b)4w@P-oz^|b%!8L)!oM~4 zCyyVs&#xZK4ziEoG@^fO&3#6gaP+>)H@esTe%RtG{H^dL>n8+?lfFQ*7vEd7?~~wfmQ=M3KFQ z3e3xO$<`ZB#k~9)>jG{3=$T9S?>a$Qca{y0vf_Wx)=QKjyW)cd_PCB>*_r3%SsJ$+ z!P30u_t#8WT&KnO9+kVJDeba#<9VAcwU@E^Tckq?)y z*CJ7#TKqQ}HdT}n*08y*KM*57ArjxVuDi~BVzs;LA3KdUox4GKEU~MgI|Q8?tsafL zF|L>2!B3xxbw2hR=wDpJJNKj2=RXy=U&+4jyN-&dV(jx*jrrH#NJW07D~ok{*Xg?` z?%)%b@ILL-uQY7^Rf-rokIK|;MH#Xw_(Gi|rQ&^lzT%4CV%Rn;JbcB&Jd1_4fHj_Xf^hDatI@v_qmiwPnN{{Zx!` z_|iPd^M*Y?IcC!@e*RD4LkE3Sy?b%4>nWig zz;^ZUmbCQv4O+wgMtY8)MI~8pO0CQDMyj_dux6xKgHJyC%q6^It{=PmKD8LWh)Pup zr_|{)i+Z3glG0?zOpqDkOaQXP*{43I^z6S4={a1iT#6JF+l@aW`;UI zX9qs5@EZi%)>B{o;#KTJmndf5GRl@Kx3wODl|#jO`IzO~DY$x_1bca4KA=W3Y9G6v z+=2O_>#ndYZvy4>FP~sNf&R#tEf>MBAME%Ln?tJ`j5r>6E`a5yQpZn?d-)st0X)~d zPqfSUFJ_4|(s9lXq5qG3v4?e+J--?oM!#V{Xjyyxm&edzeZSB>6!+>`f~9Xu_32%+ymIJ+OYXcP4qHh>h+5iSK!dovVJvQ@EiWNQ3oajP)b&Ne_0={TN%_5aZT*)iKLGZ zU>*L^)G`3+C$;CAqY3a@wZj0>2VKX{a0vfV9IprU3hNn@AV0}hv{w;RnCCov=RIfd za9j)a5AGykZBW*7)+g@y$zFA3}Ssu7)pUcllj=6v`szVb}?{Y4(&ReG40 z_qMX{Qt15R`>nY2xl8zm>`=st;r;6C7WoL|@LGI&`hWP^v-ICliMQl<yrR{&`(eV!9zdOg8K%MZu7VAd;Lso!yI`BGD>j&GB|6|zS z1oDv-)-;l!PvglsUYj@U0p+>_^6*fOby1;KqXcXG!oi``DSkUmJN1i8xF4|W3{_~< ziT!IOjJ=t=us=2IK1}HMYuEHzo3;y|{qj6chK!eC;<}IH$-HKXi2@&k z@M*2ZrYHY@@8J*BqQ~=i<~j2h;`M%j97jB#y+!34cBH{;F1mzy%=M7j=Pshe;>I_^ zH!W7anuyI6fAF?T_Px`O-4*`&kZ(N4K9KXg(Y5Ws(V~s@qI$ZF|8A^DgmOWUzeA73 z-njNex{Qm<^%y9zmK?8Kztl`WqrYtF0?0m+=lwW0ka~f#t>q&+C%`M#@5r~^u<;Jz ze>lgc!Fy#IwMmelE&L-bZz+Z6#?4z}I+mA=`E}{R@08V|-wXY@r3X%V75fYK9wWs4 zMZ4hxD;HxQzd`9T=b(Ds#?kn_4_v}M^muSCub%7EWzI$oy0Pt+i@m7+jYI$r1PJI&mH6q0fM0 zdu@fBK<@Y9F9B-+8BbiR15|(TB=Iep_a5pH{>QD~E8a7s|D)y!k#UOs2x53T5})r9 z$HNLLuh_^ymlw8jo}A~^c^`l9JvHyrOU%Fc25Gr`%*FVPeK>SzU6J97xqjGu%EP*M z$Px*J1@sJbrkdR`SrKIq0&`qiaKCj3cDLmV%uKcSa(?Z z<8QvcmK-s!trOStDC(>iCs41wGNuV^2xKf_$Xnr?Lt?Y@qTQk&%CJu-BV8Z8`_ldo zy1g`gpx58xPmA$DzmAXkI`#ca&$^Ar!HLhO&d zMCIBPRJ$2f;5ffkS!bgb%Wdnk-^Q|h_Mb6FUTZyMee|w%+jVye|B&~Wwb%{8R?VHh zJN3rBnS1ahVrk=DEzXkb|6JcDs5911X4(ipA<}0m;)=*v!lRCVX2q=Z|KZ0zUi5Ho zbZ_aIk3|1vWxq3KaW9`6y7z{_6VB_;-uaee)BmMQ7w`YXacz}3BU9FFwDH^rUco+e z_~4IEjRD|4Fsua)-*nUI`F_OaTSCuI@)G2a-`%ILvCj3&KKPNwY&|RN_OVCDe6FsC z{QV~PT`rEB7r=UaHTev;1>14<=J+mSxNm2(KbCncYf;mz%iq(mE$67s_(cN$e#}RZ zhs;lXMNAjy0^uBU;PhowBDkV>ZqU4S4l`wXktpqY3dXV~7<@3DmuX*^;M>a_*Q0hCF<6#E5;bcne-RXdp-Mu;_(53ZZ7i6$TArttn1xhd+nik&zAoW zS%%&fatM?=oX-(&Jo#Hi3NLv79{ZWBSWP^WIeQM_4^zD-_tfv*H}6xiKt&H7^#kQC z9xUwZw01OR{~x#KbE@2|r`4_&I?B+uLEiAgG3&%<1YcbN)#_Sh^0I7P-)3C`%-;=o zzV5T+U7GaX=LxW_#<~1Gf&IDx>lMP=2|gkH_p0;7?`*mA@wsI~YR$f!p|iB?#H|GK z!0jJ?Ypv@tK6{btc{A6|cYdU5d@hP|pp#-oc5Mr@?o5MR%+eSAv^Y!7|8t#VP;Z>) zne7iL`;&f7)H(o@uKpW$}AdjHA7=a*TY z!{0M2(_fg+&B{=q=d6lvMp{2$QXaSA=2xcI0DHgu(;DA0_Wf@h<^hN~DE+!F-+si3 zeei&OBjd!lNyq0y+b(_Nl{Np6sdL#6l;Yzam?>2?oW}0@%&E10{ChtV_(s}{*{nFk z96N088Vahata~IbaTT@zypC@NKkBEyw2O7{fH`c(F__nJ4cLD}LH5C-o!{XnB&cR1 zv5$b9Fo5H;qpVtyQFbvev5zZ!km0>L?YcRH{|3xs@E+zI*r#~|(`L(1#ryb;>-ef! z>?N1yxXv5Uw~oX=gUm(l|Fw32+5C!$Z?)j~b<_CAo*-{w#jnu79Ty$OKh_7*_ekwV z&FR+He+yc+?T}S}3GpaBwa%CM!cM4ByH2A0=+n2-q^GXab~%N6*xEw3U#*W{jAIjo zckWC1S#J*cOM(p^WO3c*Y^50&pP9lua6fSEF$!Tl%D32`ip&dF=pz&@SCydC7qjs_ zT6W_nt4v|=GUcomaXaBhP>pYUzsrUyd}AQJk!kLiv@aUwQAK$R@euRkUlfG z-;O62OgmB!u6GE&xfz=PHBXdh0C;!suB%qe7UVcFxCfs;dV77Nd*x~=zM!Ib4BK1p z>!rsWyC>!?oI@YK@sN&Oy-$d%i@J0jKGx&dN6ak5=S6uS*vmS5j^AVUz69s@+1W>a z=}I-MHcH4JGWiU8K&0~fzT3om_R z#k=~faLKjrtv>uQm)pa;I(^FrPsH9IYt1F>x4;bkoAw+c`V!a&&eY>Cx%`&?u(%In z9l>34dY0>c4P(HJO@O-JaSnj@vKJ~rSc3zX(g#4F_=OJPzj4=oA}{#&(8ITxdjWNt zi#&P^ilLH$if&-#It@JTk9W_lFDPH(qRLq1b${D&Yn+<%aUK9)T~fD_GT){F*nuc`H&ej={ts|9sNWX?`rJhce4TGMZNMD z^(V;k)cCgdJ9?ht8@4$60~L_YA}ZEvpErf<6TC7(qy?_jLhTKlhKyWk*8JV5yGNN%z|Ex%C3 z`n;K(@a#-o_}dEKAZBa=wEZA`g1y0*>$qU27$;eB=5`4G3s2loaO79iY5c<`P{xx- zEKDhv^TZXte{)!3mgOlH?65sP=KRKe<9V)E^u{F*1;+fD2M*bI$|1~y7eE$}KS1&C zjeOFu?oC*`-ZXB<`%YnA@&MQkAis1hPcd`Or^UbY^;MGffQ;7utJq&T_68^iy7bBE zd|x}><^NI-p6k5W8O$Z8ceo7&nf`}0ml>M?*$47kxNu?d?K0%ZBi4!dj@#~hZ<+_} zxc8HIU+5HH|HWY*0AFM|SceVe!oJ26n3s7%hd}z~f`3h%&70NV!E@0YuCpGH)m~iG z8TBqzy#Wo~bjD%akKouyB`k5*kxvGWqZBo6zOMeS#I)J#f(uVXe#p0f{33_ve((b6 z=fC^JdAj(AE>M9IiqBB&CynCN$g!4OuF?*k`=(uT!m_h<;dY34;m-IEar41cI)U!}fHJWtfTZgdUV)$a~ z8U=ZP0yvfe^l6RO^G5f_AN-igv(2~|JjHs`a)3=arv+e_53WckW={d#EU zef*DjL-13Mea&*#5z@~83r~GWx%`y51GXh*^-z)n4^Y|s<#!fy$>lvBhs~Iv!ysfD z&csHBE`*c^BA-EXS389N5|;H}_Z0^n!aw|hrDho!%0&DeP+zoGh)w$;k?)9I#ShO{eC6*tbdD;6T<-)06cZ-K*EWyXaH+5il$7!*kN_X!#noJcfP95;J60^a*fA z=g8T9@A*eq7bt@qFO)T3A?h)Ihr_s^ap7BP$T84TSz_}+o>r+zJJaWPW^s?_pz~9p zq++{(IDLB9HggZYiZQNwGapdnf6A^?LY{y$_|8a5-yP2I0>sBb-I;bcgS+JL4%Z#N zfZnmRgMx#DN%}Fg6)VhMf8o3qd174}xA~xH9x!OmD)C;aT1`#ke+cWeLthnoKc2Gb z+j8MHrO%j2)U9~MdNk$u_omkT(A^CRixhhD=nur)3m-I5*QU;BjgR|tSsv1I^jy~Y zQGDGXuhM*$KjEE=Zk_Md`~mSmQBF42J=*-iS5Dy{vO=sKaF)61>O(935wAs^3J_yWaQi#%tA|1@dO;EKLw>CzT?4#%!oeEPO2 z98CVUOV+JYdFqpIZF_jOI9NX6AXlXL_@|4?G0+ z`f~1%VxPuXx8OeP=90=^pQ}70Lk8=5uAHOTOX}yj?`tr9hi?G;fA}s}_eZ&M${CWY zvC{M9Q|!%g7P#`_J-a*rc0g$}D*JeCYymxfAkP}%$Hxe*#tn|tWl>tWm0J~C4qYSCj*lFy8A3$dr zW32zIm9&BN1w+|h2>*ky)>MzVl=K_tW0rROldrEQHJ%+QpX=L)>EEb7{Cex1^`p8qye&sOsq2~uXJmdkY(lm^8XR$7}e4CSwU@s}3<@&zCwA@l} z^7r?*x%Pq|bv$b?R*=1KgL4F_Bb|@+#y8%2YLf>bb~wsKED^mqU*3~(KnwW?5aMOI zd+iT@U8o1_Y2h>9-ueE(mFKPTFrf$4dfY;XaWCe4)}NMhe!9$AX~3eLUco)q5;#A= zIs@(Q5;MhV?1MjaAFJqr!%ttfyo!z_&W6In!&5Z&G2T^iekUg-rp=bk-!mK2S$tRM zD?@f^54ZAs1N>TVjbQJ6bvoWhEJ2iCnBzli{p6V;516#=hu53vC3o&Rp^GKr1L%X_*MS~E-X6#7^f-0*EeE* zeTgvzpIr?bax6FIVP+^#o;(7J&~r5Nysc;O?WG@af7T^|zMow4tFO=*1oja70{(=w z*vJ1}mh2Vv@+;==cI@MfKXux4B7gYcw>9s>KR;xG=vxr{<;=SLox|84y74q+%K6-H zX5rvUUYqkJ_B+Kc5^YGs{*+x=_k#zFTB^hWl>GuZUrLnmYsWKuj7!-#w+-jzna$3@ zN%0)luO-vJn1s)s7A;yhgk$&x#JgCxxkFA1-7cvgRHk}udgt?Bb$I~%hoW5Q^XuWi z6#JeaZICH5yDxJjujJ_LtyOl3KbJ9FK3>uakXC)Q8ym^-vMOp5;@6N8QtzL7=W68JBb z6u351Cf98glhEmb?`~)LuRQ0dQKN=Kyu;`l!FQ0jKsm!I%>GW|*zc*{ zn%>qu_~|LvsI@2$V+sU42Kd{Nxb(&qK7#!$HbbG!+PH*$tQ-7FDDlg%ZgQ#dh;2s6XblWF`mGuaF*vvKTF@n@cHLTKI(TN1ClYst2b@U zJmB{PJYX=#^FsagY=P^u{tVJ;H*4uO>|+hlptW)~l_^I)8n^cg-MN4KyAQ3ly6~d` zT|CI_Y~frlZS@#~;$RXg%6?H9JvmR~#^hYZXfbUR629S!>{y-6HBn#`#;vIL&{f5NP5qYL!EDl|J zSr_~8-wCn-=^DNTX}%A1!~WJtE3D7c0p(#{Q8{x_7Atr|L_Tn z*dfTbsSM%5azZZMadgMA*%fB!45 z&i#nli&&s2zg54HPGNuP@f%b$P|+2GOx)GArn|gfWd~4(TfSPvg zPn|hNI)1~q_Qbu{yo&uLSHB}ai+>>4=j{CGJBRU)*q&+FKOx$H^`Rr@O70v#p2`a` z2IQK?NcJUV0^A2l7W13SuFv!bz6136+&Mq1`xg8T{Q*Aj?wD&hSCRSw&=p?z-Uq^F z0BwR@FMNB9<@0gaxr*2#w;#9#|NSOR72`L3*4(w`d3Yv)}w}Ud2B43sstR5cSE)I+^P)f8h}JA>VHp)l3P&6yyBAo$;boYIs1CvBdf*YECf zz_(-!b;QJ;w&yhE=6F76ThBE|31fWn~m) z`d6#x)Vcn&b5E$4B{muS5ymXpLW_@mz%joST`2h8b!GqV%)9MI&lYV;#d{vaMfc)?-`?|(HDW$N{?Hq*+)%MYsm{J{ONJ@BX`|*XcNza@p8ih7 zgMvjlh?%X`@#!`15TA#q73pQ_MmXeaTh9NJPd*T_^3awHS+ddeohNAX^~cuO9c9A8 zy)N$?vF@l<{($&Fj$nW2>{ax(MK20^Tq(gnpS|&>mo9z!As&VqeHm38j4PhPx;9@P zQ$3mfhq2{I+#bF5I(kpq24c_diMayNZT5W%i`EX)(%-O+LAiRaaOezUgkn6sn|oBOA_7OU?5pUW7E z?*K{)8%lNjKj?LO;yWNu_#l++l-G<|vbv0Y`0>HHBlg}s=N?Qj_eaHTw9b#!+w?LO zD^tr8VB??N5(CYzcqv-TzAb_MHUmeA_8_Lm9TlN^By;3*GVIGgJ5< zzF@sryP*GZo?NL?C69e?PmLq9`I_DH>*ZIbjN&JW`)QxM@vhE~^M{`6a zSFKt#kBte$Bf_|oZ7y4_rc3yTj$h%Dif(b0$leLC-*NI*A)iB8b-G16Jm-TC#IoPg zY!;hj?31OhZ0Q3JIt{R|b!Ol0>^tz|6~ysC(Jtut$U50t`>#2Fk8nOI`yFzbh=>S} zeP=yPdHbI9<%@GJdwYIu-M~96;}rU~#nqT~*Kf{u0KNz2E$lh8b@2B8(*GIc0iGNi z@IN4BESR@l@$27lu%hdhB~PIQ{`;Ww^CtV|LjOXiPdlfZ`G3OUJCwx|d!z{4I7+$t z*cCghF+JLknd?>TgJ)Ea>@3=avpO^KzQOFPBOUuwL0_Q1q5tXavE}XZ)bzK|CiPnyNID{2J6~q;D@@L`4oBNlb+v8}QE%1S{`)^xqcS{6UwCd2~tSkfj??B#_nXfpzes}ggtTj+J z^d({zZ?=hj>`!3#i|4VPdy)6~Mp$vjd6qo8;(nayD(>+<|CM0+k;!F0JVxzy^}dY% z2i}KxXs+gG?|tZC!#6SdBsHHw#I4+9mj_7w{%rY*Ca~+P+M=(>Cq2i@L0oL@yl(G* z?DsR~Q0(?gR;WUVbBB2!`)BCZppJ-}2OB$QaqP@{@Dm7G1Iq6^X}%%$m%ewCq)OiU;^T_vhQL@Bn^47tW#A@lp$@h z&_AJ#o6(+|pX>60V_$qnI5)s^o#MA!=l)hP%fvnC6gm@YFaGqG)-yg}U;4<#89(;_ zC=W3=7he3@tJsfTb4ch(qWpG)$LeAqb&TvijL$h!iF(2oGdb-mogIhC;XUc=j{Co_ zv-szC(=pw|H^koBS=)VY^#^zp+6_C2I(6!Jd<`Jm1=*C;M|zui&wz;w#vwKkeOzj+ScQMMX*l((=P+#aiIq4`}!?DA@DzD*FBD- zkyCu58twZ!jQu&+exj0WtBdc0^R!iG?^tmb7SUeZPn$6dP1^T?SFs_gs{&tK7lDHc-GVcovD?0*1f1o%FX?JYWT&5C{K z)ZzY{(DUDT!mHTFyq_+Uvd4jb4{UVg`TnvKx2Q_P2vIlG5As@1>TD#{_fJmcLN^C} z&2-KY{PXign108*(0xfxZAuAs!9EH4&{A(0{rC^Ixq}Ta*8a_hC^B)J)nWf`Prj#3 zuhn_5_^xkr{Jj}FPg*gC_;@G_ajyC=+~HO1kKJ~T(pX}jmZ@5cw%&Lu>V-Xii$2lT zJwEhPpi`OL_mU}LoY|`*G+OVpLs)u?r3{b;`3NTscbU@PI4A5 z>9AgZ)Ra|1w*zNtu!Gab{#LKf`y)3Tx5ntJRJRGOVLOAi14mnR_b`?qJ9eiZsFXMc zlB&)!eg=OB_~yuBj|cEG|MS)vdQ$aCQj`lFC9Hd)+X+m7au(*;M^jc4WO>>(zV`pY z>rYtE)4GkBmMpLjzkSkQcS!9<)U?|Gt8FgYhqIWJ9%Cyh#)`Ykgg%93%*B|%KY2rk z&fRTHDfvN%7;+h@uZZuGh0o{^SKBCN2lRW=Wl?16;hp;GV!!XaEnc1Xhps#zWQ?eP zZniCcgY_3IV@&v{c84BYO8KVAy`GRsTfPnA@7VMY|-wkU+PI!2V3Q0%?$f*=r2lMfGccgB6|&`6{kKTZ(|>N!(AsYC;y5y6W|{5 zO!T8CZLV$gl`qY)bp~YlDjyc5jd;L|+^_JfB+ulF7A=}=Z5i;bpc{krw3ZiuA3*-! zcGPT-`R!YH=^GlfWH(i8+{S7*jB-&IoC#w;nVjuFUgRTT**Hryo<3b zF$JCT^hrw8M};4B*g3*) z6l8E2GiJ2zZ?u@U^*?wCcnb2t9w{aEg?60#{@3SQs(T|B06!|L7;o`!cR5_P##5R38tpg`bq3 zra22NspbJGy=~6cAM0ir?+1Hjtd*1d*?_Zc^}XlgZ>qDvWj_2r9}n>5C3OTK7lhr8 z4=>OA$JZAxMqfbB*CG@CCtov29^m`F9SP@yQZ5KtiBG5f#TfIhj%~=!UnF}*)mh+B zAN~`Lamd9pXIA3Rz{UkJ4}AN9gni^Ho>I#D*e@flJ?2Zy3n|4HroH|R3k#ELfX}&Y z;w;eKJ1`DXz;GUbOqr0m51c=RJt$gZ{yJAoPS0lOA(E>}aq)f~~A9VF^yr0XPVEnnrR!;ex_4Q zSC~F#dc^cC(|1f*XCR*V&rH8EA=VJ|kl>^IFQ$Jp{fFs)OcGm=PvHOmG9eAmfc|E} zJK#-tAHEHc7xMg#3HkrRgt9rwbLk>B`5mF+t+))0FY<5my%FXKHP8*egYV9LoU&z$aBr?_hcR~@)f=(Uq-CEtIdr0+)Oo@ zz!R`W5&Qu1609|S<1FcT0dOvLx`BW20iSJ0!tVh;)b7H<%bpKoKMT_YCaX*p=Y_uc zCgGU#^n42WGuHQ#7ehYd^Hr|@4X|H**>UUc_Vc)Vo2ft37jg_jo``c2pL{@n9J+%i z;5zDDY&lcbE;Z?z_r}}}%A+U|`@z;O}<5i49*cf75`}jcO`5yksOO-06wp$f_#(f9f zj?GL{nKGLFM*QqsOf8xAF1YHYj=zs>fW?>YDL1s;EOna(qbF%A7g=vTkY z@@mLPVUGuWDrti)`-iVzGc9AP$MlA)W0Ld0naxlp=!1!GfVc>d8NLkIM;l%3L*M(Z z^93GzVN56F7{nEN$M8%1GMpJe4g!4%#AlOnnpAyppMTF}f(|Ma)`Wc3omb8`9n)kc zq0r!??Ac7;9V}^tqtVF7&~{iy)tQk#0@aQ;=le*I*rXN*kUWlxUdfa`^Q z8P+1A?ywuC86Iol4VwoK_#!guC+LXWF26KqJuy5&W(B}fV1z*onF{D8+Hbc!l5 z&1SOd8S`%|Pk`+K-I$Xaar)kaG7ZlQzs_KF+C!yPwH>Z?FwQ- zAB%o~E)w=&azCBYu1VdWzVZ{!?e6Fg9-FT-m1XM3w1(+0(>10?Oo{v!@I5_DtM9?K z`75R?OnaG@FpXvE!c>mQ<+FHqwA-6`aGTpPS@&UBYo)|k5#H=GUs>+Qx-)&s<2DVG zA5#S;=paGPRn_u8<~M()!c6&?vN4HRG0v3rBEQ4^f_{YlA=W=4jwEEVutiKM>wp*e z`}o?HJowFv{v8hrVZh!yDSeV9MPK;J^kML29RrO0H<;Qmot1n5SJ*@)h0pkx^*i|5?c+g7 zJq8%}0Zh=PLfk4LBY@8noE>`0H(^rufv4dBqhU+s>3C8v zd*A!o@8dtoX$&y_VdJ@-$?BUB_88D3fc=&knZ6^>B&W9f>f*zqZwx47fbpM`X&lp6 zOjcVA*c-uDth@X{aeveQaRIu2B0MO*h{2ysk26Sr4Y~8*8H4mxN-{k%NY7wNhjz8z zUja)xv@Z4jLM`b=4UDykOgT1P>(2{*_R5x=#HIsMB>uK!S9g5A^)& za7OS6uK7#Q{zsZb{x?DTnZ)Vp&!dL`2_DgmxWw;^*xa&>I?3vEo$h(%m00;Qkxuup zKlJD6I^B~XU8j3qd8NSf_wA)W(yPDf1Dw%IS9NflopinAS5zNBH~sks(Ct1I{B^sJ znX>%T?LO59u$QjeeX0-eNVopaKfoC~>2dn$)(>DWUH^3}0`$A@IYJD&&-#Ik(&Yy- z=sxQQGUz_*2eOy`$e?`^L1*lw#~G!EDg(q`y3y;F4{X%E5`o6)$_F;;UXFGx>Sxrw z@&g)npJf0Trz;42WY|7?>B;~&WB9(39%q=YAk<#E@$1R}u#;}w{R#&#?tTT~_R@_% zk1_!4r5k@9<$YV}7$A>qwBJnn85{3|#oJ4dv-7^KbPO;XuL}-jC*9WP#rv+N+xqQ_{gb5WZot2*{KM}1_V|~p>1S+=U-XZs(;wL= zAN=3dbX)Tm@^>}e76U~7uBO`pFy>cR(`^9|`Ma8KYk(tvTj>SNq{kT#Ao0Gd>1XWZ zZ!i6k@%y1@pR4IM2C&HAUb>9|D&Dt|9?R9YH2~d`ZeswVcv}Nhq}v!^?n#d``oKKB z;m_4{qwFmkTw?|R_HBy+De1=JLrH&RSiX{O*gVSy-tcv0e`w?Lf_>T=ULxJb@D%kk zuD(*gGsg83-!INix^c4jJ~o;s1Q5nwk2ALU^*BA&_Vk7HM>gsQ5#Jd*>2bEw3)tk| z*gx8AJ_{IZHlNdNd|j?s2Ai`t1Qd^KRo`yt#-}mu}~E`^mu**1 z^9u017H7#ooqk4U8v2}VtJ7r#Fzw0T>U7DA3-C=@0d=}8RNT`k-;$}Ab!BfPcwZ?X zR@{4}WRM_TaAUFNx2{SOrgIl4YYOYCPLrBr1L_-^-=YGVd+L8h1=KgRzwP8NdRzOB`v1Rd*H6st{|~e%vjqSE literal 0 HcmV?d00001 diff --git a/resources/loxodo-qt.svg b/resources/loxodo-qt.svg new file mode 100755 index 0000000..f659b2d --- /dev/null +++ b/resources/loxodo-qt.svg @@ -0,0 +1,529 @@ + + + + + Logo + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Logo + + + Okami + + + + + Okami + + + + + Okami + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/qt-bw.svg b/resources/qt-bw.svg new file mode 100755 index 0000000..e186189 --- /dev/null +++ b/resources/qt-bw.svg @@ -0,0 +1,618 @@ + + + + + + Logo + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Logo + + + Okami + + + + + Okami + + + + + Okami + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/qt.svg b/resources/qt.svg new file mode 100755 index 0000000..498a096 --- /dev/null +++ b/resources/qt.svg @@ -0,0 +1,535 @@ + + + + + Logo + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Logo + + + Okami + + + + + Okami + + + + + Okami + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/setup.py b/setup.py index f48537f..dbffd81 100755 --- a/setup.py +++ b/setup.py @@ -8,58 +8,108 @@ Usage (Windows): python setup.py py2exe """ - +import operator +import os import sys -from setuptools import setup +from setuptools import setup, find_packages + + +def read(fname): + return open(os.path.join(os.path.dirname(__file__), fname)).read() + + +def get_data_files(data_dirs): + data_files = [] + for dest, src in data_dirs: + for subdir in map(operator.itemgetter(0), os.walk(src)): # find all subdirs dirs + data_files += map(lambda x: (os.path.relpath(subdir, dest), (x,)), # py2exe config formatting + filter(os.path.isfile, # files only + map(lambda x: os.path.join(subdir, x), os.listdir(subdir)))) # every file + return data_files + +METADATA = { + 'name': 'loxodo', + 'version': '1.0', + 'author': 'Christoph Sommer', + 'author_email': 'mail@christoph-sommer.de', + 'description': '''Password Safe V3 compatible Password Vault.''', + 'license': 'GPLv2+', + 'keywords': 'loxodo password safe', + 'url': 'http://www.christoph-sommer.de/loxodo', + 'long_description': read('README.txt'), + 'classifiers': [ + 'Development Status :: 6 - Mature', + 'Environment :: Console', + 'Environment :: MacOS X', + 'Environment :: Win32 (MS Windows)', + 'Environment :: X11 Applications :: Qt', + 'Intended Audience :: End Users/Desktop', + 'License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 2 :: Only', + 'Topic :: Security :: Cryptography', + ], + 'packages': find_packages(), + 'scripts': ['loxodo.py'], + 'data_files': get_data_files(( + ('.', 'resources'), + ('.', 'locale'), + )), + # 'install_requires': ['PyQt4>=4.10.4'], + 'include_package_data': True, +} if sys.platform == 'darwin': - extra_options = dict( - name="Loxodo", - setup_requires = ['py2app'], - app = ['loxodo.py'], - options = dict( - py2app = dict( - argv_emulation = True, - iconfile = 'resources/loxodo-icon.icns', - packages = ['src', 'wx'], - site_packages = True, - resources = ['resources', 'locale', 'LICENSE.txt', 'README.txt'] - ) - ) - ) - setup(**extra_options) -elif sys.platform == 'win32': + METADATA.update({ + 'name': 'Loxodo', + 'setup_requires': ['py2app'], + 'app': ['loxodo.py'], + 'options': { + 'py2app': { + 'argv_emulation': True, + 'iconfile': 'resources/loxodo-icon.icns', + 'packages': ['src', 'wx'], + 'site_packages': True, + 'resources': [ + 'resources', 'locale', 'LICENSE.txt', 'README.txt' + ], + } + } + }) +elif sys.platform in ('win32', 'cygwin'): import py2exe - import os - - # create list of needed data files - dataFiles = [] - for subdir in ('resources', 'locale'): - for root, dirs, files in os.walk(subdir): - if not files: - next - files = [] - for filename in files: - files.append(os.path.join(root, filename)) - if not files: - next - dataFiles.append((root, files)) + import PyQt4 - extra_options = dict( - setup_requires = ['py2exe'], - windows = ['loxodo.py'], - data_files = dataFiles, - options = dict( - py2exe = dict( - excludes = 'ppygui' - ) - ) + pyqt4_dir = os.path.dirname(PyQt4.__file__) + data_dirs = ( + (os.path.join(pyqt4_dir, 'plugins'), os.path.join(pyqt4_dir, 'plugins', 'imageformats')), ) - setup(**extra_options) -else: - extra_options = dict( - scripts = ['loxodo.py'], - ) - setup(**extra_options) - + data_files = get_data_files(data_dirs) + METADATA.update({ + 'setup_requires': ['py2exe'], + 'windows': [{ + 'script':'loxodo.py', + 'icon_resources': [(1, 'resources/loxodo-qt.ico')], + }], + 'data_files': METADATA['data_files'] + data_files, + 'zipfile': None, + 'options': { + 'py2exe': { + 'bundle_files': 3, + 'compressed': True, + 'includes': [ + 'sip', + 'PyQt4.QtSvg', + 'PyQt4.QtXml', + ], + 'excludes': 'ppygui', + 'dll_excludes': [ + 'w9xpopen.exe', + 'MSVCP90.dll', + ], + } + } + }) +if __name__ == '__main__': + setup(**METADATA) diff --git a/src/config.py b/src/config.py index a7cf60f..15e7f2a 100755 --- a/src/config.py +++ b/src/config.py @@ -26,6 +26,11 @@ class Config(object): """ Manages the configuration file """ + FRONTENDS = ( + 'wx', + 'qt4', + ) + def __init__(self): """ DEFAULT VALUES @@ -37,8 +42,12 @@ def __init__(self): self.search_notes = False self.search_passwd = False self.alphabet = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_" + self.frontend = 'wx' + self.favicon = False + self.window_width = 800 + self.window_height = 480 - self._fname = self.get_config_filename() + self._fname, self._cache = self.get_config_files() self._parser = SafeConfigParser() if os.path.exists(self._fname): @@ -70,6 +79,19 @@ def __init__(self): if self._parser.get("base", "search_passwd") == "True": self.search_passwd = True + if self._parser.has_option('base', 'frontend'): + self.frontend = self._parser.get('base', 'frontend') + + if self._parser.has_option('base', 'favicon'): + if self._parser.get('base', 'favicon') == 'True': + self.favicon = True + + if self._parser.has_option('base', 'window_width'): + self.window_width = int(self._parser.get('base', 'window_width')) + + if self._parser.has_option('base', 'window_height'): + self.window_height = int(self._parser.get('base', 'window_height')) + if not os.path.exists(self._fname): self.save() @@ -97,41 +119,66 @@ def save(self): self._parser.set("base", "alphabetreduction", str(self.reduction)) self._parser.set("base", "search_notes", str(self.search_notes)) self._parser.set("base", "search_passwd", str(self.search_passwd)) + self._parser.set('base', 'frontend', self.frontend) + self._parser.set('base', 'favicon', str(self.favicon)) + self._parser.set('base', 'window_width', str(self.window_width)) + self._parser.set('base', 'window_height', str(self.window_height)) filehandle = open(self._fname, 'w') self._parser.write(filehandle) filehandle.close() + def get_cache_dir(self): + if not os.path.exists(self._cache): + os.mkdir(self._cache) + return self._cache + @staticmethod - def get_config_filename(): + def get_config_files(): """ Returns the full filename of the config file + and fill path to cache directory """ base_fname = "loxodo" + base_cache = 'cache' + + # Default configuration path is ~/.config/foo/ + base_path = os.path.join(os.path.expanduser("~"), ".config") + if os.path.isdir(base_path): + fname = os.path.join(base_path, base_fname, base_fname + ".ini") + else: + # ~/.foo/ + fname = os.path.join(os.path.expanduser("~"), "." + base_fname + ".ini") + + # ~/.cache/foo/ + base_path = os.path.join(os.path.expanduser('~'), '.cache') + if os.path.isdir(base_path): + cache = os.path.join(base_path, base_fname) + else: + # ~/.foo/cache/ + cache = os.path.join(os.path.expanduser('~'), '.' + base_fname, base_cache) # On Mac OS X, config files go to ~/Library/Application Support/foo/ if platform.system() == "Darwin": base_path = os.path.join(os.path.expanduser("~"), "Library", "Application Support") if os.path.isdir(base_path): - return os.path.join(base_path, base_fname, base_fname + ".ini") + fname = os.path.join(base_path, base_fname, base_fname + ".ini") + cache = os.path.join(base_path, base_fname, base_cache) # On Microsoft Windows, config files go to $APPDATA/foo/ if platform.system() in ("Windows", "Microsoft"): if ("APPDATA" in os.environ): base_path = os.environ["APPDATA"] if os.path.isdir(base_path): - return os.path.join(base_path, base_fname, base_fname + ".ini") + fname = os.path.join(base_path, base_fname, base_fname + ".ini") + cache = os.path.join(base_path, base_fname, base_cache) # Allow config directory override as per freedesktop.org XDG Base Directory Specification if ("XDG_CONFIG_HOME" in os.environ): base_path = os.environ["XDG_CONFIG_HOME"] if os.path.isdir(base_path): - return os.path.join(base_path, base_fname, base_fname + ".ini") + fname = os.path.join(base_path, base_fname, base_fname + ".ini") + cache = os.path.join(base_path, base_fname, base_cache) - # Default configuration path is ~/.config/foo/ - base_path = os.path.join(os.path.expanduser("~"), ".config") - if os.path.isdir(base_path): - return os.path.join(base_path, base_fname, base_fname + ".ini") - else: - return os.path.join(os.path.expanduser("~"),"."+ base_fname + ".ini") + return fname, cache config = Config() diff --git a/src/frontends/qt4/__init__.py b/src/frontends/qt4/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/src/frontends/qt4/favicon.py b/src/frontends/qt4/favicon.py new file mode 100755 index 0000000..71ada16 --- /dev/null +++ b/src/frontends/qt4/favicon.py @@ -0,0 +1,147 @@ +# +# Loxodo -- Password Safe V3 compatible Password Vault +# Copyright (C) 2014 Okami +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +import os +import re +import time +import Queue + +from hashlib import md5 + +from HTMLParser import HTMLParser + +from PyQt4 import QtGui +from PyQt4.QtNetwork import (QNetworkAccessManager, QNetworkCookieJar, + QNetworkRequest) +from PyQt4.QtCore import (Qt, QUrl, QEventLoop, QThread, SIGNAL) + +from .settings import COLUMNS_BY_FIELD + +from ...config import config + + +class FaviconFinder(HTMLParser): + def __init__(self): + HTMLParser.__init__(self) + self.favicon_url = None + + def handle_starttag(self, tag, attrs): + attributes = dict(attrs) + if tag == 'link' and 'href' in attributes and 'icon' in attributes.get('rel'): + if not self.favicon_url: + self.favicon_url = attributes['href'] + + +class FaviconUpdater(QThread): + def __init__(self): + super(FaviconUpdater, self).__init__() + self.cache = config.get_cache_dir() + self.queue = Queue.Queue() + self.running = True + self.faviconReady = SIGNAL('faviconReady') + + def __del__(self): + self.wait() + + def stop_me(self): + self.running = False + + def _get(self, url): + loop = QEventLoop() + request = QNetworkRequest() + request.setAttribute(QNetworkRequest.CacheLoadControlAttribute, + QNetworkRequest.PreferCache); + request.setRawHeader('User-agent', + 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64)') + request.setUrl(url) + cookies = QNetworkCookieJar() + manager = QNetworkAccessManager() + manager.setCookieJar(cookies) + reply = manager.get(request) + reply.finished.connect(loop.quit) + loop.exec_() + data = reply.readAll() + code, ok = reply.attribute( + QNetworkRequest.HttpStatusCodeAttribute).toInt() + if code in (301, 302): + redirect = reply.attribute( + QNetworkRequest.RedirectionTargetAttribute).toUrl() + return self._get(redirect) + else: + return data + + def _parse_favicon(self, page): + favicon_url = None + #### HTMLParser method + # finder = FaviconFinder() + # finder.feed(page) + # favicon_url = finder.favicon_url + #### BeautifulSoup method + # from BeautifulSoup import BeautifulSoup + # soup = BeautifulSoup(page) + # link = soup.html.head.find( + # lambda x: x.name == 'link' and 'icon' in x['rel']) + # if link: + # favicon_url = link['href'] + #### RegExp method + if not favicon_url: + link = r'(?P]+rel=[\'\"][\w ]*icon[\'\"][^>]*/?>)' + href = r'href=[\'\"](?P[^\'\"]+)[\'\"]' + s = re.search(link, str(page)) + if s and s.group('link'): + s = re.search(href, s.group('link')) + if s and s.group('href'): + favicon_url = s.group('href') + return favicon_url + + def _get_favicon(self, item): + url = item.data(COLUMNS_BY_FIELD['url'], Qt.EditRole).toString() + path = os.path.join(self.cache, md5(str(url)).hexdigest()) + if os.path.exists(path): + size = os.path.getsize(path) + if size <= 1: + return + f = open(path) + favicon = f.read() + f.close() + else: + page = self._get(QUrl(url)) + favicon_url = self._parse_favicon(str(page)) or '/favicon.ico' + favicon = self._get(QUrl(url).resolved(QUrl(favicon_url))) + f = open(path, 'w+') + f.write(favicon) + f.close() + image = QtGui.QImage() + image.loadFromData(favicon) + self.emit(self.faviconReady, item, image) + + def run(self): + while self.running: + try: + item = self.queue.get() + try: + self._get_favicon(item) + except Exception as e: + print(e) + # pass + self.queue.task_done() + except Queue.Empty: + time.sleep(0.1) + + def on_url_updated(self, item): + self.queue.put(item) diff --git a/src/frontends/qt4/loadframe.py b/src/frontends/qt4/loadframe.py new file mode 100755 index 0000000..c276eea --- /dev/null +++ b/src/frontends/qt4/loadframe.py @@ -0,0 +1,163 @@ +# +# Loxodo -- Password Safe V3 compatible Password Vault +# Copyright (C) 2014 Okami +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# + +import os + +from PyQt4 import QtGui +from PyQt4.QtCore import Qt + +from .settings import VAULT_EXT, ALL_EXT + +from ...config import config +from ...vault import Vault + + +class LoadFrame(QtGui.QDialog): + ''' + Displays the "welcome" dialog which lets the user open a Vault. + ''' + def __init__(self, parent=None, is_new=True): + super(LoadFrame, self).__init__(parent) + + path = os.path.dirname(os.path.realpath( + config.get_basescript())) + logo = QtGui.QLabel(self) + logo.setPixmap(QtGui.QPixmap( + os.path.join(path, 'resources', 'qt-bw.svg'))) + logo.setScaledContents(True) + + self._tc_passwd = QtGui.QLineEdit(self) + self._tc_passwd.setEchoMode(QtGui.QLineEdit.Password) + + if is_new: + self._tc_passwd2 = QtGui.QLineEdit(self) + self._tc_passwd2.setEchoMode(QtGui.QLineEdit.Password) + create = QtGui.QPushButton('Create', self) + create.clicked.connect(self._on_new) + else: + self._fb_filename = QtGui.QComboBox(self) + self._fb_filename.setEditable(True) + if config.recentvaults: + self._fb_filename.addItems(config.recentvaults) + open_ = QtGui.QPushButton('Open', self) + open_.clicked.connect(self._on_open) + browse = QtGui.QPushButton('Browse', self) + browse.clicked.connect(self._on_pickvault) + + cancel = QtGui.QPushButton('Cancel', self) + cancel.clicked.connect(self.close) + + grid = QtGui.QGridLayout() + grid.setColumnStretch(1, 1) + grid.setSpacing(10) + + grid.addWidget(logo, 0, 0, 1, 4, + alignment=Qt.AlignHCenter | Qt.AlignBottom) + + if is_new: + grid.addWidget(QtGui.QLabel('Password' + ':', self), 1, 0) + grid.addWidget(self._tc_passwd, 1, 1, 1, 3) + grid.addWidget(QtGui.QLabel('Retype password' + ':', self), 2, 0) + grid.addWidget(self._tc_passwd2, 2, 1, 1, 3) + grid.addWidget(create, 3, 2) + grid.addWidget(cancel, 3, 3) + else: + grid.addWidget(QtGui.QLabel('Vault' + ':', self), 1, 0) + grid.addWidget(self._fb_filename, 1, 1, 1, 2) + grid.addWidget(browse, 1, 3) + grid.addWidget(QtGui.QLabel('Password' + ':', self), 2, 0) + grid.addWidget(self._tc_passwd, 2, 1, 1, 3) + grid.addWidget(open_, 3, 2) + grid.addWidget(cancel, 3, 3) + + if is_new: + self.setWindowTitle('Loxodo - ' + 'Create Vault') + else: + self.setWindowTitle('Loxodo - ' + 'Open Vault') + + self.setLayout(grid) + + # self.updateGeometry() + # size = self.size() + # self.resize(size) + # size.setWidth(size.width() * 2) + # print(self.size()) + + self._tc_passwd.setFocus() + + def _on_pickvault(self): + home = os.path.expanduser("~") + wildcard = ";;".join(VAULT_EXT.keys() + ALL_EXT.keys()) + filename, filter_ = QtGui.QFileDialog.getOpenFileNameAndFilter(self, + caption='Open file', directory=home, filter=wildcard) + if filename: + i = self._fb_filename.findText(filename) + if i >= 0: + self._fb_filename.setCurrentIndex(i) + else: + self._fb_filename.addItems([filename]) + self._tc_passwd.setFocus() + + def _on_new(self): + password = unicode(self._tc_passwd.text()).encode( + 'latin1', 'replace') + password2 = unicode(self._tc_passwd2.text()).encode( + 'latin1', 'replace') + if password != password2: + QtGui.QMessageBox.warning(self, 'Bad Password', + 'The given passwords does not match', + QtGui.QMessageBox.Ok | QtGui.QMessageBox.Default, + QtGui.QMessageBox.NoButton) + self._tc_passwd.setFocus() + self._tc_passwd.selectAll() + else: + self.parentWidget().open_vault(password=password) + self.close() + + def _on_open(self): + try: + password = unicode(self._tc_passwd.text()).encode( + 'latin1', 'replace') + filename = unicode(self._fb_filename.currentText()) + self.parentWidget().open_vault( + filename=filename, password=password) + if (filename in config.recentvaults + and config.recentvaults.index(filename) != 0): + config.recentvaults.remove(filename) + if filename not in config.recentvaults: + config.recentvaults.insert(0, filename) + config.save() + self.close() + except Vault.BadPasswordError: + QtGui.QMessageBox.warning(self, 'Bad Password', + 'The given password does not match the Vault', + QtGui.QMessageBox.Ok | QtGui.QMessageBox.Default, + QtGui.QMessageBox.NoButton) + self._tc_passwd.setFocus() + self._tc_passwd.selectAll() + except Vault.VaultVersionError: + QtGui.QMessageBox.warning(self, 'Bad Vault', + 'This is not a PasswordSafe V3 Vault', + QtGui.QMessageBox.Ok | QtGui.QMessageBox.Default, + QtGui.QMessageBox.NoButton) + except (Vault.VaultFormatError, IOError): + QtGui.QMessageBox.warning(self, 'Bad Vault', + 'Vault integrity check failed', + QtGui.QMessageBox.Ok | QtGui.QMessageBox.Default, + QtGui.QMessageBox.NoButton) diff --git a/src/frontends/qt4/loxodo.py b/src/frontends/qt4/loxodo.py new file mode 100755 index 0000000..f51a5e5 --- /dev/null +++ b/src/frontends/qt4/loxodo.py @@ -0,0 +1,48 @@ +# +# Loxodo -- Password Safe V3 compatible Password Vault +# Copyright (C) 2014 Okami +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# + +import os +import platform +import sys + +from PyQt4 import Qt +from PyQt4 import QtGui + +from .vaultframe import VaultFrame + +from ...config import config + + +def main(): + # set taskbar icon in windows + if platform.system() == 'Windows': + import ctypes + APPID = 'okami.loxodo.qt.1' + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(APPID) + app = QtGui.QApplication(sys.argv) + cpath = os.path.dirname(os.path.realpath(config.get_basescript())) + ipath = os.path.join(cpath, 'resources', 'loxodo-qt.svg') + qicon = QtGui.QIcon(ipath) + app.setWindowIcon(qicon) + mainframe = VaultFrame() + mainframe.show() + app.exec_() + + +main() diff --git a/src/frontends/qt4/recordframe.py b/src/frontends/qt4/recordframe.py new file mode 100755 index 0000000..d96d8b5 --- /dev/null +++ b/src/frontends/qt4/recordframe.py @@ -0,0 +1,202 @@ +# +# Loxodo -- Password Safe V3 compatible Password Vault +# Copyright (C) 2014 Okami +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# + +import os +import random +import struct + +from PyQt4 import QtGui +from PyQt4.QtCore import Qt, QVariant + +from .settings import COLUMNS, COLUMNS_BY_FIELD + +from ...config import config + + +class RecordFrame(QtGui.QDialog): + ''' + Displays (and lets the user edit) a single Vault Record. + ''' + def __init__(self, parent): + super(RecordFrame, self).__init__(parent) + self.vaultframe = parent + + self.resize(200, 200) + + self._tc_title = QtGui.QLineEdit(self) + self._tc_group = QtGui.QComboBox(self) + self._tc_group.setEditable(True) + self._tc_user = QtGui.QLineEdit(self) + self._tc_passwd = QtGui.QLineEdit(self) + self._tc_passwd.setEchoMode(QtGui.QLineEdit.Password) + self._bt_showhide = QtGui.QPushButton('un(mask)', self) + self._bt_showhide.clicked.connect(self._on_toggle_passwd_mask) + self._bt_generate = QtGui.QPushButton('generate', self) + self._bt_generate.clicked.connect(self._on_generate_passwd) + self._tc_url = QtGui.QLineEdit(self) + self._tc_notes = QtGui.QTextEdit(self) + + self._bt_ok = QtGui.QPushButton('OK', self) + self._bt_ok.clicked.connect(self._on_ok) + self._bt_cancel = QtGui.QPushButton('Cancel', self) + self._bt_cancel.clicked.connect(self._on_cancel) + + grid = QtGui.QGridLayout() + grid.setSpacing(10) + + grid.addWidget(QtGui.QLabel('Title' + ':', self), 0, 0) + grid.addWidget(self._tc_title, 0, 1, 1, 3) + grid.addWidget(QtGui.QLabel('Group' + ':', self), 1, 0) + grid.addWidget(self._tc_group, 1, 1, 1, 3) + grid.addWidget(QtGui.QLabel('Username' + ':', self), 2, 0) + grid.addWidget(self._tc_user, 2, 1, 1, 3) + grid.addWidget(QtGui.QLabel('Password' + ':', self), 3, 0) + grid.addWidget(self._tc_passwd, 3, 1) + grid.addWidget(self._bt_showhide, 3, 2) + grid.addWidget(self._bt_generate, 3, 3) + grid.addWidget(QtGui.QLabel('URL' + ':', self), 5, 0) + grid.addWidget(self._tc_url, 5, 1, 1, 3) + grid.addWidget(QtGui.QLabel('Notes' + ':', self), 6, 0, + alignment=Qt.AlignTop) + grid.addWidget(self._tc_notes, 6, 1, 1, 3) + grid.addWidget(self._bt_ok, 7, 2) + grid.addWidget(self._bt_cancel, 7, 3) + + self.setWindowTitle('Loxodo - ' + 'Edit Vault Record') + self.setLayout(grid) + + self.set_initial_focus() + + self._vault_record = None + + def update_fields(self): + ''' + Update fields from source + ''' + if self._vault_record: + for column in COLUMNS: + if column['field'] != 'group': + tc = getattr(self, '_tc_%s' % column['field']) + column = COLUMNS_BY_FIELD[column['field']] + tc.setText(self._vault_record.data( + column, Qt.EditRole).toString()) + + list_ = self._vault_record.treeWidget() + items = list(list_.groups()) + if items: + self._tc_group.addItems([''] + items) + + group = self._vault_record.data(COLUMNS_BY_FIELD['group'], + Qt.EditRole).toString() + i = self._tc_group.findText(group) + if i >= 0: + self._tc_group.setCurrentIndex(i) + else: + self._tc_group.addItems([group]) + + def _apply_changes(self): + ''' + Update source from fields + ''' + if self._vault_record: + for column in COLUMNS: + if column['field'] not in ['group', 'notes']: + tc = getattr(self, '_tc_%s' % column['field']) + column = COLUMNS_BY_FIELD[column['field']] + self._vault_record.setData(column, Qt.EditRole, + QVariant(tc.text())) + + self._vault_record.setData(COLUMNS_BY_FIELD['group'], + Qt.EditRole, + QVariant(self._tc_group.currentText())) + + self._vault_record.setData(COLUMNS_BY_FIELD['notes'], + Qt.EditRole, + QVariant(self._tc_notes.toPlainText())) + + def _on_cancel(self): + ''' + Event handler: Fires when user chooses this button. + ''' + self.reject() + + def _on_ok(self): + ''' + Event handler: Fires when user chooses this button. + ''' + self._apply_changes() + self.accept() + + def _on_toggle_passwd_mask(self): + if self._tc_passwd.echoMode() == QtGui.QLineEdit.Normal: + self._tc_passwd.setEchoMode(QtGui.QLineEdit.Password) + else: + self._tc_passwd.setEchoMode(QtGui.QLineEdit.Normal) + + def _on_generate_passwd(self, dummy): + _pwd = self.generate_password( + alphabet=config.alphabet, + pwd_length=config.pwlength, + allow_reduction=config.reduction) + self._tc_passwd.setText(_pwd) + + @staticmethod + def _urandom(count): + try: + return os.urandom(count) + except NotImplementedError: + retval = "" + for dummy in range(count): + retval += struct.pack(" +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# + +import os + +from PyQt4 import QtGui +from PyQt4.QtCore import Qt + +from ...config import config + +COLUMNS = ({ + 'label': 'Title', + 'field': 'title', +}, { + 'label': 'Username', + 'field': 'user', +}, { + 'label': 'Group', + 'field': 'group', +}, { + 'label': 'Password', + 'field': 'passwd', +}, { + 'label': 'URL', + 'field': 'url', +}, { + 'label': 'Notes', + 'field': 'notes', +}) + +COLUMNS_BY_FIELD = dict(map( + lambda (i, column): (column['field'], i), enumerate(COLUMNS))) + +VAULT_EXT = { + 'Vault (*.psafe3)': '.psafe3', +} +ALL_EXT = { + 'All files (*.*)': '.*', +} + + +def icon_from_resources(name): + cpath = os.path.dirname(os.path.realpath(config.get_basescript())) + ipath = os.path.join(cpath, 'resources', 'icons', name + '.svg') + if os.path.exists(ipath): + return QtGui.QIcon.fromTheme(name, QtGui.QIcon(ipath)) + else: + return QtGui.QIcon.fromTheme(name) + + +class Settings(QtGui.QDialog): + def __init__(self, parent=None): + super(Settings, self).__init__(parent) + + self._frontend = QtGui.QComboBox(self) + self._frontend.addItems(config.FRONTENDS) + + # self._search_notes = QtGui.QCheckBox('Search inside notes', self) + # self._search_passwd = QtGui.QCheckBox( + # 'Search inside passwords', self) + + self._sc_length = QtGui.QSpinBox(self) + self._sc_length.setRange(4, 128) + + self._cb_reduction = QtGui.QCheckBox( + 'Avoid easy to mistake chars', self) + + self._tc_alphabet = QtGui.QLineEdit(config.alphabet, self) + + self._favicon = QtGui.QCheckBox( + 'Download website icons for the records', self) + + self._bt_ok = QtGui.QPushButton('OK', self) + self._bt_ok.clicked.connect(self._on_ok) + self._bt_cancel = QtGui.QPushButton('Cancel', self) + self._bt_cancel.clicked.connect(self._on_cancel) + + grid = QtGui.QGridLayout() + grid.setSpacing(10) + + grid.addWidget(QtGui.QLabel('Frontend', self), 0, 0) + grid.addWidget(self._frontend, 0, 1, 1, 3) + # grid.addWidget(self._search_notes, 1, 0, 1, 4) + # grid.addWidget(self._search_passwd, 2, 0, 1, 4) + grid.addWidget(QtGui.QLabel( + 'Generated Password Length', self), 3, 0) + grid.addWidget(self._sc_length, 3, 1, 1, 3) + grid.addWidget(QtGui.QLabel('Alphabet', self), 4, 0) + grid.addWidget(self._tc_alphabet, 4, 1, 1, 3) + grid.addWidget(self._cb_reduction, 5, 0, 1, 4) + grid.addWidget(self._favicon, 6, 0, 1, 4) + + grid.addWidget(self._bt_ok, 7, 2) + grid.addWidget(self._bt_cancel, 7, 3) + + self.setWindowTitle('Loxodo - ' + 'Settings') + self.setLayout(grid) + + self.set_initial_focus() + self.update_fields() + + def update_fields(self): + ''' + Update fields from source + ''' + i = self._frontend.findText(config.frontend) + if i >= 0: + self._frontend.setCurrentIndex(i) + else: + self._frontend.addItems([config.frontend]) + self._sc_length.setValue(config.pwlength) + self._tc_alphabet.setText(config.alphabet) + self._cb_reduction.setChecked(config.reduction) + # self._search_notes.setChecked(config.search_notes) + # self._search_passwd.setChecked(config.search_passwd) + self._favicon.setChecked(config.favicon) + + def _apply_changes(self): + ''' + Update source from fields + ''' + config.frontend = str(self._frontend.currentText()) + config.pwlength = self._sc_length.value() + config.reduction = self._cb_reduction.isChecked() + # config.search_notes = self._search_notes.isChecked() + # config.search_passwd = self._search_passwd.isChecked() + config.alphabet = unicode(self._tc_alphabet.text()) + config.favicon = self._favicon.isChecked() + config.save() + + def _on_cancel(self, dummy): + ''' + Event handler: Fires when user chooses this button. + ''' + self.reject() + + def _on_ok(self, evt): + ''' + Event handler: Fires when user chooses this button. + ''' + self._apply_changes() + self.accept() + + def set_initial_focus(self): + self._sc_length.setFocus() diff --git a/src/frontends/qt4/vaultframe.py b/src/frontends/qt4/vaultframe.py new file mode 100755 index 0000000..5e66819 --- /dev/null +++ b/src/frontends/qt4/vaultframe.py @@ -0,0 +1,618 @@ +# +# Loxodo -- Password Safe V3 compatible Password Vault +# Copyright (C) 2014 Okami +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# + +import re +import os + +from PyQt4 import QtGui +from PyQt4.QtCore import (Qt, QVariant, QTimer, QUrl, QObject, QEventLoop, + QThread, SIGNAL, QBuffer) + +from .favicon import FaviconUpdater +from .loadframe import LoadFrame +from .recordframe import RecordFrame +from .settings import (Settings, COLUMNS, COLUMNS_BY_FIELD, VAULT_EXT, ALL_EXT, + icon_from_resources) + +from ...config import config +from ...vault import Vault + + +class VaultFrame(QtGui.QMainWindow): + ''' + Displays (and lets the user edit) the Vault. + ''' + class VTWidgetRecordItem(QtGui.QTreeWidgetItem): + ''' + QTreeWidgetItem that contains the contents of a Record. + ''' + def __init__(self, *args, **kwargs): + self._record = None + super(VaultFrame.VTWidgetRecordItem, + self).__init__(*args, **kwargs) + self.setFlags(self.flags() | Qt.ItemIsEditable) + self.setIcon(0, icon_from_resources('text-x-generic')) + + def type_(self): + return 'record' + + def data(self, column, role): + ''' + Overrides the base classes method. + ''' + # column 10 is reserved for Record + if column == 10: + return self._record + elif role in (Qt.DisplayRole, Qt.EditRole): + field = COLUMNS[column]['field'] + value = self._record and getattr(self._record, field) + return QVariant(value) + else: + return super(VaultFrame.VTWidgetRecordItem, + self).data(column, role) + + def setData(self, column, role, value): + ''' + Overrides the base classes method. + ''' + # column 10 is reserved for Record + if column == 10: + self._record = value + tree = self.treeWidget() + tree.emit(tree.urlUpdated, self) + elif role in (Qt.DisplayRole, Qt.EditRole): + if self._record: + field = COLUMNS[column]['field'] + old_value = getattr(self._record, field) + value = unicode(value.toString()) + setattr(self._record, field, value) + if old_value != value: + self.emitDataChanged() + if field == 'url': + tree = self.treeWidget() + tree.emit(tree.urlUpdated, self) + + else: + return super(VaultFrame.VTWidgetRecordItem, + self).setData(column, role, value) + + class VTWidgetGroupItem(QtGui.QTreeWidgetItem): + ''' + QTreeWidgetItem that contains the contents of a Group. + ''' + def __init__(self, *args, **kwargs): + super(VaultFrame.VTWidgetGroupItem, + self).__init__(*args, **kwargs) + self.setFlags(self.flags() | Qt.ItemIsEditable) + self.setIcon(0, icon_from_resources('folder')) + + def type_(self): + return 'group' + + def setData(self, column, role, value): + ''' + Overrides the base classes method. + ''' + if column == 0 and role in ( + Qt.DisplayRole, Qt.EditRole): + if not value.toString(): + value = QVariant('') + if self.data(column, role) != value: + # update childs + field = COLUMNS[column]['field'] + for child in map(self.child, range(self.childCount())): + if child.type_() == 'record': + child.setData(COLUMNS_BY_FIELD['group'], + Qt.EditRole, value) + return super(VaultFrame.VTWidgetGroupItem, + self).setData(column, role, value) + + class VaultTreeWidget(QtGui.QTreeWidget): + ''' + QTreeWidget that contains the contents of a Vault. + ''' + def __init__(self, *args, **kwargs): + self.vault = None + self.urlUpdated = SIGNAL('urlUpdated') + super(VaultFrame.VaultTreeWidget, self).__init__(*args, **kwargs) + self.setHeaderLabels(map(lambda x: x['label'], COLUMNS[:2])) + self.setColumnWidth(0, 250) + self.setSortingEnabled(True) + self.sortItems(0, Qt.AscendingOrder) + + def on_favicon_ready(self, item, image): + try: + icon = QtGui.QIcon(QtGui.QPixmap(image)) + if icon.isNull(): + icon = icon_from_resources('text-x-generic') + item.setIcon(0, icon) + except: + item.setIcon(0, icon_from_resources('text-x-generic')) + + def update_fields(self): + self.clear() + groups = {} + for record in self.vault.records: + if record.group and not record.group in groups: + groups[record.group] = VaultFrame.VTWidgetGroupItem( + self, [record.group]) + if record.group: + parent = groups[record.group] + else: + parent = self + item = VaultFrame.VTWidgetRecordItem( + parent, type=QtGui.QTreeWidgetItem.Type) + item.setData(10, Qt.EditRole, record) + + def set_vault(self, vault): + ''' + Set the Vault this control should display. + ''' + self.vault = vault + self.update_fields() + self.select_first() + + def deselect_all(self): + ''' + De-selects all items + ''' + for item in self.selectedItems(): + self.setItemSelected(item, False) + + def select_first(self): + ''' + Selects and focuses the first item (if there is one) + ''' + self.deselect_all() + if self.topLevelItemCount() > 0: + self.setItemSelected(self.topLevelItem(0), True) + + def groups(self): + return map(lambda x: x.text(0), + filter(lambda x: x.type_() == 'group', + map(self.topLevelItem, range(self.topLevelItemCount())))) + + def move_to_group(self, item): + old_group = '' + if item.parent(): + old_group = item.parent().text(0) + new_group = item.data(COLUMNS_BY_FIELD['group'], + Qt.EditRole).toString() + if old_group == new_group: + return + self.take_child_from_parent(item) + if new_group: + if new_group in self.groups(): + for i in map(self.topLevelItem, range(self.topLevelItemCount())): + if i.type_() == 'group' and i.text(0) == new_group: + group = i + break + else: + group = VaultFrame.VTWidgetGroupItem(self, [new_group]) + group.addChild(item) + else: + self.addTopLevelItem(item) + + def take_child_from_parent(self, child): + parent = child.parent() + if parent: # take from parent + parent.takeChild(parent.indexOfChild(child)) + else: # take from root + self.takeTopLevelItem(self.indexOfTopLevelItem(child)) + + def __init__(self, *args, **kwargs): + super(VaultFrame, self).__init__(*args, **kwargs) + self.resize(config.window_width, config.window_height) # KILL ME + + self.list_ = VaultFrame.VaultTreeWidget() + self.list_.itemChanged.connect(self.mark_modified) + self.list_.itemDoubleClicked.connect(self._on_list_item_activated) + + self.updater = None + if config.favicon: + self.updater = FaviconUpdater() + # signal: Updater(Thread) -> Widget(GUI) + QObject.connect(self.updater, self.updater.faviconReady, + self.list_.on_favicon_ready) + # signal: Widget(GUI) -> Updater(Thread) + QObject.connect(self.list_, self.list_.urlUpdated, + self.updater.on_url_updated) + self.updater.start() + + self.statusBar() + + # Set up menus + new = QtGui.QAction(icon_from_resources('document-new'), + '&New', self) + new.setShortcut('Ctrl+N') + new.triggered.connect(self._on_new) + + open_ = QtGui.QAction(icon_from_resources('document-open'), + '&Open', self) + open_.setShortcut('Ctrl+O') + open_.triggered.connect(self._on_open) + + self.save = QtGui.QAction(icon_from_resources('document-save'), + '&Save', self) + self.save.setShortcut('Ctrl+S') + self.save.triggered.connect(self._on_save) + + save_as = QtGui.QAction(icon_from_resources('document-save-as'), + 'Save as' + '...', self) + save_as.setShortcut('Ctrl+Shift+S') + save_as.triggered.connect(self._on_save_as) + + change_password = QtGui.QAction( + 'Change &Password' + '...', self) + change_password.triggered.connect(self._on_change_password) + + open_settings = QtGui.QAction('&Settings', self) + open_settings.triggered.connect(self._on_settings) + + exit_ = QtGui.QAction(icon_from_resources('exit'), + 'E&xit', self) + exit_.setShortcut('Ctrl+Q') + exit_.triggered.connect(self.close) + + add_group = QtGui.QAction(icon_from_resources('folder-new'), + 'Add &group', self) + add_group.setShortcut('Ctrl+G') + add_group.triggered.connect(self._on_group_add) + + add_record = QtGui.QAction(icon_from_resources('contact-new'), + '&Add record', self) + add_record.setShortcut('Ctrl+A') + add_record.triggered.connect(self._on_add) + + edit_record = QtGui.QAction(icon_from_resources('document-properties'), + '&Edit', self) + edit_record.setShortcut('Ctrl+E') + edit_record.triggered.connect(self._on_edit) + + remove_record = QtGui.QAction(icon_from_resources('edit-delete'), + '&Delete', self) + remove_record.setShortcut('Ctrl+Backspace') + remove_record.triggered.connect(self._on_delete) + + copy_username = QtGui.QAction(icon_from_resources('edit-copy'), + 'Copy &Username', self) + copy_username.setShortcut('Ctrl+U') + copy_username.triggered.connect(self._on_copy_username) + + copy_password = QtGui.QAction(icon_from_resources('edit-copy'), + 'Copy &Password', self) + copy_password.setShortcut('Ctrl+P') + copy_password.triggered.connect(self._on_copy_password) + + open_url = QtGui.QAction(icon_from_resources('internet-web-browser'), + 'Open UR&L', self) + open_url.setShortcut('Ctrl+L') + open_url.triggered.connect(self._on_open_url) + + menubar = self.menuBar() + + file_ = menubar.addMenu('&File') + file_.addAction(new) + file_.addAction(open_) + file_.addAction(self.save) + file_.addAction(save_as) + file_.addSeparator() + file_.addAction(change_password) + file_.addSeparator() + file_.addAction(open_settings) + file_.addSeparator() + file_.addAction(exit_) + + edit = menubar.addMenu('&Edit') + edit.addAction(add_group) + edit.addAction(add_record) + edit.addAction(remove_record) + edit.addSeparator() + edit.addAction(edit_record) + edit.addSeparator() + edit.addAction(copy_username) + edit.addAction(copy_password) + edit.addAction(open_url) + + toolbar = self.addToolBar('Toolbar') + toolbar.addAction(new) + toolbar.addAction(open_) + toolbar.addAction(self.save) + toolbar.addSeparator() + toolbar.addAction(add_group) + toolbar.addAction(add_record) + toolbar.addAction(remove_record) + toolbar.addAction(edit_record) + toolbar.addSeparator() + # toolbar.addAction(copy_username) + toolbar.addAction(copy_password) + toolbar.addAction(open_url) + + self.setWindowTitle('Loxodo - ' + 'Vault Contents') + + self.setCentralWidget(self.list_) + + self.vault_file_name = None + self.vault_password = None + self.vault = None + self.mark_modified(is_modified=False) + + def mark_modified(self, item=None, i=0, is_modified=True): + self._is_modified = is_modified + self.save.setEnabled(is_modified) + + def open_vault(self, filename=None, password=''): + ''' + Set the Vault that this frame should display. + ''' + self.vault_file_name = None + self.vault_password = None + self.vault = Vault(password, filename=filename) + self.list_.set_vault(self.vault) + self.vault_file_name = filename + self.vault_password = password + self.mark_modified(is_modified=False) + self.statusBar().showMessage('Read Vault contents from disk') + + def save_vault(self, filename, password): + ''' + Write Vault contents to disk. + ''' + try: + self.mark_modified(is_modified=False) + self.vault_file_name = filename + self.vault_password = password + self.vault.write_to_file(filename, password) + self.statusBar().showMessage('Wrote Vault contents to disk') + except RuntimeError: + QtGui.QMessageBox.critical(self, 'Error writing to disk', + 'Could not write Vault contents to disk', + QtGui.QMessageBox.Ok | QtGui.QMessageBox.Default, + QtGui.QMessageBox.NoButton) + + def _clear_clipboard(self, match_text=None): + clipboard = QtGui.QApplication.clipboard() + if match_text: + if clipboard.text() != match_text: + return + clipboard.clear() + self.statusBar().showMessage('Cleared clipboard') + + def _copy_to_clipboard(self, text, duration=None): + clipboard = QtGui.QApplication.clipboard() + clipboard.setText(text) + if duration: + QTimer().singleShot(duration * 1000, + lambda: self._clear_clipboard(text)) + + def _on_list_item_activated(self, item, column): + ''' + Event handler: Fires when user double-clicks a list entry. + ''' + if item.type_() == 'record': + # self.list_.editItem(item, column) + self.list_.deselect_all() + self.list_.setItemSelected(item, True) + self._on_edit() + elif item.type_() == 'group': + self.list_.editItem(item, 0) + # if column == 0: + # self.list_.editItem(item, 0) + # else: + # item.setExpanded(item.isExpanded()) + + def _on_settings(self): + ''' + Event handler: Fires when user chooses this menu item. + ''' + settings = Settings(self) + # settings.resize(self._modal_size.width(), settings.size().height()) + settings.exec_() + + def _on_change_password(self): + if not self.vault: + return + value, ok = QtGui.QInputDialog.getText(self, 'Change Vault Password', + 'New password', mode=QtGui.QLineEdit.Password) + if not ok: + return + password_new = unicode(value).encode('latin1', 'replace') + value, ok = QtGui.QInputDialog.getText(self, 'Change Vault Password', + 'Re-enter new password', mode=QtGui.QLineEdit.Password) + if not ok: + return + password_new_confirm = unicode(value).encode('latin1', 'replace') + if password_new_confirm != password_new: + QtGui.QMessageBox.critical(self, 'Bad Password', + 'The given passwords do not match', + QtGui.QMessageBox.Ok | QtGui.QMessageBox.Default, + QtGui.QMessageBox.NoButton) + return + self.vault_password = password_new + self.statusBar().showMessage('Changed Vault password') + self.mark_modified() + + def _on_new(self): + loadframe = LoadFrame(self, is_new=True) + loadframe.exec_() + self._modal_size = loadframe.size() + + def _on_open(self): + loadframe = LoadFrame(self, is_new=False) + loadframe.exec_() + self._modal_size = loadframe.size() + + def _on_save(self): + if not self.vault: + return + if not self.vault_file_name: + self._on_save_as() + else: + self.save_vault(self.vault_file_name, self.vault_password) + + def _on_save_as(self): + if not self.vault: + return + home = os.path.expanduser("~") + wildcard = ';;'.join(VAULT_EXT.keys() + ALL_EXT.keys()) + filename, filter_ = QtGui.QFileDialog.getSaveFileNameAndFilter(self, + caption='Save new Vault as...', directory=home, filter=wildcard) + if filename: + filename = unicode(filename) + filter_ = unicode(filter_) + if filter_ in VAULT_EXT: + ext = VAULT_EXT[filter_] + if not filename.endswith(ext): + filename += ext + if filename not in config.recentvaults: + config.recentvaults.insert(0, filename) + config.save() + self.save_vault(filename, self.vault_password) + + def closeEvent(self, event): + ''' + Event handler: Fires when user chooses this menu item. + ''' + # TODO: ask before closing + if self.updater: + self.updater.stop_me() + if (config.window_width != self.size().width() + or config.window_height != self.size().height()): + config.window_width = self.size().width() + config.window_height = self.size().height() + config.save() + super(VaultFrame, self).closeEvent(event) + + def _on_group_add(self): + if not self.vault: + return + item = VaultFrame.VTWidgetGroupItem(self.list_, ['New Group']) + self.list_.editItem(item) + + def _on_edit(self): + ''' + Event handler: Fires when user chooses this menu item. + ''' + item = self.list_.currentItem() + if self.vault and item: + if item.type_() == 'record': + old_group = item.data(COLUMNS_BY_FIELD['group'], + Qt.EditRole).toString() + recordframe = RecordFrame(self) + recordframe.vault_record = item + recordframe.resize(self._modal_size.width(), + recordframe.size().height()) + recordframe.exec_() + new_group = item.data(COLUMNS_BY_FIELD['group'], + Qt.EditRole).toString() + if old_group != new_group: + # self.list_.update_fields() + self.list_.move_to_group(item) + elif item.type_() == 'group': + self.list_.editItem(item) + + def _on_add(self): + ''' + Event handler: Fires when user chooses this menu item. + ''' + if not self.vault: + return + entry = self.vault.Record.create() + item = VaultFrame.VTWidgetRecordItem( + self.list_, type=QtGui.QTreeWidgetItem.Type) + item.setData(10, Qt.EditRole, entry) + recordframe = RecordFrame(self) + recordframe.vault_record = item + recordframe.resize(self._modal_size.width(), + recordframe.size().height()) + if recordframe.exec_() == QtGui.QDialog.Accepted: + self.vault.records.append(entry) + # self.list_.update_fields() + self.list_.move_to_group(item) + else: + self.list_.takeTopLevelItem( + self.list_.indexOfTopLevelItem(item)) + + def _on_delete(self): + ''' + Event handler: Fires when user chooses this menu item. + ''' + item = self.list_.currentItem() + if self.vault and item: + if item.type_() == 'record': + entry = item.data(10, Qt.EditRole) + if entry.user or entry.passwd: + reply = QtGui.QMessageBox.question(self, + 'Really delete record?', + ('Are you sure you want to delete this record? ' + 'It contains a username or password and there is ' + 'no way to undo this action.'), + QtGui.QMessageBox.Yes, QtGui.QMessageBox.No) + if reply != QtGui.QMessageBox.Yes: + return + self.vault.records.remove(entry) + elif item.type_() == 'group': + # move everyone in this group to root + children = map(item.child, range(item.childCount())) + for child in children: + child.setData(COLUMNS_BY_FIELD['group'], + Qt.EditRole, QVariant('')) + self.list_.take_child_from_parent(child) + self.list_.addTopLevelItems(children) + self.list_.take_child_from_parent(item) + + def _on_copy_username(self): + ''' + Event handler: Fires when user chooses this menu item. + ''' + item = self.list_.currentItem() + if self.vault and item and item.type_() == 'record': + title = item.data(COLUMNS_BY_FIELD['title'], Qt.EditRole).toString() + user = item.data(COLUMNS_BY_FIELD['user'], Qt.EditRole).toString() + self._copy_to_clipboard(user) + self.statusBar().showMessage( + 'Copied username of "%s" to clipboard' % title) + + def _on_copy_password(self): + ''' + Event handler: Fires when user chooses this menu item. + ''' + item = self.list_.currentItem() + if self.vault and item and item.type_() == 'record': + title = item.data(COLUMNS_BY_FIELD['title'], Qt.EditRole).toString() + passwd = item.data(COLUMNS_BY_FIELD['passwd'], Qt.EditRole).toString() + self._copy_to_clipboard(passwd, duration=10) + self.statusBar().showMessage( + 'Copied password of "%s" to clipboard' % title) + + def _on_open_url(self): + ''' + Event handler: Fires when user chooses this menu item. + ''' + item = self.list_.currentItem() + if self.vault and item and item.type_() == 'record': + url = unicode(item.data(COLUMNS_BY_FIELD['url'], + Qt.EditRole).toString()) + try: + import webbrowser + webbrowser.open(url) + except ImportError: + self.statusBar().showMessage( + 'Could not load python module ' + '"webbrowser" needed to open "%s"' % url) From 3aca28fdc8965e31e70666489cd9842bfcaaf11a Mon Sep 17 00:00:00 2001 From: Okami Date: Tue, 5 Aug 2014 11:25:27 +0400 Subject: [PATCH 2/5] frontend switching option --- MANIFEST.in | 0 Makefile | 0 debian/changelog | 0 debian/clean | 0 debian/compat | 0 debian/control | 0 debian/copyright | 0 debian/links | 0 debian/loxodo.desktop | 0 debian/loxodo.install | 0 debian/rules | 0 debian/source/format | 0 loxodo.py | 46 +++++++++++------------------------ src/config.py | 20 +++++++++++---- src/frontends/qt4/settings.py | 2 +- src/frontends/wx/settings.py | 18 ++++++++++++++ 16 files changed, 48 insertions(+), 38 deletions(-) mode change 100755 => 100644 MANIFEST.in mode change 100755 => 100644 Makefile mode change 100755 => 100644 debian/changelog mode change 100755 => 100644 debian/clean mode change 100755 => 100644 debian/compat mode change 100755 => 100644 debian/control mode change 100755 => 100644 debian/copyright mode change 100755 => 100644 debian/links mode change 100755 => 100644 debian/loxodo.desktop mode change 100755 => 100644 debian/loxodo.install mode change 100755 => 100644 debian/rules mode change 100755 => 100644 debian/source/format diff --git a/MANIFEST.in b/MANIFEST.in old mode 100755 new mode 100644 diff --git a/Makefile b/Makefile old mode 100755 new mode 100644 diff --git a/debian/changelog b/debian/changelog old mode 100755 new mode 100644 diff --git a/debian/clean b/debian/clean old mode 100755 new mode 100644 diff --git a/debian/compat b/debian/compat old mode 100755 new mode 100644 diff --git a/debian/control b/debian/control old mode 100755 new mode 100644 diff --git a/debian/copyright b/debian/copyright old mode 100755 new mode 100644 diff --git a/debian/links b/debian/links old mode 100755 new mode 100644 diff --git a/debian/loxodo.desktop b/debian/loxodo.desktop old mode 100755 new mode 100644 diff --git a/debian/loxodo.install b/debian/loxodo.install old mode 100755 new mode 100644 diff --git a/debian/rules b/debian/rules old mode 100755 new mode 100644 diff --git a/debian/source/format b/debian/source/format old mode 100755 new mode 100644 diff --git a/loxodo.py b/loxodo.py index 6d90f85..6f956bb 100755 --- a/loxodo.py +++ b/loxodo.py @@ -18,36 +18,18 @@ else: config.set_basescript(unicode(__file__, sys.getfilesystemencoding())) +if '-cmdline' in sys.argv: + from src.frontends.cmdline import loxodo + sys.exit() + +frontend = config.frontend +# invalid frontend, select first one +if not frontend in config.frontends: + config.frontend = config.frontends[0] -frontends = list(config.FRONTENDS) -# change frontends priority using config -if config.frontend in frontends and frontends.index(config.frontend) > 0: - frontends.remove(config.frontend) - frontends.insert(0, config.frontend) - - -for frontend in frontends: - # update current frontend - config.frontend = frontend - if frontend == 'wx': - try: - import wx - from src.frontends.wx import loxodo - sys.exit() - except ImportError as e: - print('Could not find wxPython, the wxWidgets Python bindings: %s' % e) - print('Falling to the next frontend.') - print('') - elif frontend == 'qt4': - try: - import PyQt4 - from src.frontends.qt4 import loxodo - sys.exit() - except ImportError as e: - print('Could not find PyQt4, the Qt4 Python bindings: %s' % e) - print('Falling to the next frontend.') - print('') - - -from src.frontends.cmdline import loxodo -sys.exit() +if frontend == 'wx': + from src.frontends.wx import loxodo + sys.exit() +elif frontend == 'qt4': + from src.frontends.qt4 import loxodo + sys.exit() diff --git a/src/config.py b/src/config.py index 15e7f2a..b13d578 100755 --- a/src/config.py +++ b/src/config.py @@ -26,15 +26,25 @@ class Config(object): """ Manages the configuration file """ - FRONTENDS = ( - 'wx', - 'qt4', - ) - def __init__(self): """ DEFAULT VALUES """ + # available frontends + self.frontends = [] + try: + import wx + except ImportError as e: + pass + else: + self.frontends.append('wx') + try: + import PyQt4 + except ImportError as e: + pass + else: + self.frontends.append('qt4') + self._basescript = None self.recentvaults = [] self.pwlength = 10 diff --git a/src/frontends/qt4/settings.py b/src/frontends/qt4/settings.py index faf8cc7..ec3d151 100755 --- a/src/frontends/qt4/settings.py +++ b/src/frontends/qt4/settings.py @@ -69,7 +69,7 @@ def __init__(self, parent=None): super(Settings, self).__init__(parent) self._frontend = QtGui.QComboBox(self) - self._frontend.addItems(config.FRONTENDS) + self._frontend.addItems(config.frontends) # self._search_notes = QtGui.QCheckBox('Search inside notes', self) # self._search_passwd = QtGui.QCheckBox( diff --git a/src/frontends/wx/settings.py b/src/frontends/wx/settings.py index 8b9145f..da1a629 100644 --- a/src/frontends/wx/settings.py +++ b/src/frontends/wx/settings.py @@ -43,6 +43,9 @@ def __init__(self, parent): _sz_fields.AddGrowableCol(1) _sz_fields.AddGrowableRow(5) + self._cb_frontend = self._add_a_combobox( + _sz_fields, _('Frontend') + ':', config.frontend, config.frontends) + self._search_notes = self._add_a_checkbox(_sz_fields,_("Search inside notes") + ":") self._search_passwd = self._add_a_checkbox(_sz_fields,_("Search inside passwords") + ":") @@ -82,6 +85,19 @@ def __init__(self, parent): self.set_initial_focus() self.update_fields() + def _add_a_combobox( + self, parent_sizer, label, default_value, choices, extrastyle=0): + _label = wx.StaticText(self.panel, -1, label, style=wx.ALIGN_RIGHT) + parent_sizer.Add( + _label, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_RIGHT | wx.ALL, 5) + control = wx.ComboBox( + self.panel, -1, value=default_value, choices=choices, + style=wx.CB_READONLY) + parent_sizer.Add( + control, 1, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_LEFT | wx.ALL | + wx.EXPAND, 5) + return control + def _add_a_checkbox(self, parent_sizer, label, extrastyle=0): _label = wx.StaticText(self.panel, -1, label, style=wx.ALIGN_RIGHT) parent_sizer.Add(_label, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_RIGHT|wx.ALL, 5) @@ -115,6 +131,7 @@ def update_fields(self): """ Update fields from source """ + self._cb_frontend.SetValue(config.frontend) self._sc_length.SetValue(config.pwlength) self._tc_alphabet.SetValue(config.alphabet) self._cb_reduction.SetValue(config.reduction) @@ -125,6 +142,7 @@ def _apply_changes(self, dummy): """ Update source from fields """ + config.frontend = self._cb_frontend.GetValue() config.pwlength = self._sc_length.GetValue() config.reduction = self._cb_reduction.GetValue() config.search_notes = self._search_notes.GetValue() From 74c1826fb555067b28a68756ea84cc376a23e535 Mon Sep 17 00:00:00 2001 From: Okami Date: Tue, 5 Aug 2014 11:34:19 +0400 Subject: [PATCH 3/5] cmdline argv fix --- loxodo.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/loxodo.py b/loxodo.py index 6f956bb..9689b3c 100755 --- a/loxodo.py +++ b/loxodo.py @@ -18,7 +18,7 @@ else: config.set_basescript(unicode(__file__, sys.getfilesystemencoding())) -if '-cmdline' in sys.argv: +if set(sys.argv) & {'-i', '-h'}: from src.frontends.cmdline import loxodo sys.exit() @@ -33,3 +33,7 @@ elif frontend == 'qt4': from src.frontends.qt4 import loxodo sys.exit() + +# fallback frontend +from src.frontends.cmdline import loxodo +sys.exit() From 152b536b34ff9fb25b854171e8ccd260c3ab3a0c Mon Sep 17 00:00:00 2001 From: Okami Date: Tue, 5 Aug 2014 17:17:26 +0400 Subject: [PATCH 4/5] separate wx and pyqt4 debian packages --- Makefile | 9 +- __main__.py | 1 + debian/control | 18 +- debian/{loxodo.desktop => loxodo-qt4.desktop} | 6 +- debian/loxodo-qt4.docs | 3 + debian/loxodo-qt4.install | 2 + debian/{links => loxodo-qt4.links} | 0 debian/loxodo-wx.desktop | 12 + debian/loxodo-wx.docs | 3 + debian/loxodo-wx.install | 2 + debian/loxodo-wx.links | 1 + debian/loxodo.install | 2 - debian/rules | 5 +- locale/loxodo.pot | 174 +++--- loxodo.py | 8 + resources/__init__.py | 0 resources/icons/__init__.py | 0 resources/icons/contact-new.svg | 0 resources/icons/document-new.svg | 0 resources/icons/document-open.svg | 0 resources/icons/document-properties.svg | 0 resources/icons/document-save-as.svg | 0 resources/icons/document-save.svg | 0 resources/icons/edit-copy.svg | 0 resources/icons/edit-delete.svg | 0 resources/icons/folder-new.svg | 0 resources/icons/folder.svg | 0 resources/icons/internet-web-browser.svg | 0 resources/icons/text-x-generic.svg | 0 resources/{qt-bw.svg => loxodo-qt-bw.svg} | 0 .../{loxodo-qt.ico => loxodo-qt-icon.ico} | Bin resources/loxodo-qt-icon.svg | 529 +++++++++++++++++ resources/loxodo-qt.svg | 238 ++++---- resources/qt.svg | 535 ------------------ setup.py | 2 +- src/__init__.py | 3 + src/frontends/qt4/loadframe.py | 8 +- src/frontends/qt4/loxodo.py | 8 +- src/frontends/qt4/settings.py | 13 +- 39 files changed, 831 insertions(+), 751 deletions(-) create mode 100644 __main__.py rename debian/{loxodo.desktop => loxodo-qt4.desktop} (57%) create mode 100644 debian/loxodo-qt4.docs create mode 100644 debian/loxodo-qt4.install rename debian/{links => loxodo-qt4.links} (100%) create mode 100644 debian/loxodo-wx.desktop create mode 100644 debian/loxodo-wx.docs create mode 100644 debian/loxodo-wx.install create mode 100644 debian/loxodo-wx.links delete mode 100644 debian/loxodo.install mode change 100644 => 100755 debian/rules create mode 100644 resources/__init__.py create mode 100644 resources/icons/__init__.py mode change 100755 => 100644 resources/icons/contact-new.svg mode change 100755 => 100644 resources/icons/document-new.svg mode change 100755 => 100644 resources/icons/document-open.svg mode change 100755 => 100644 resources/icons/document-properties.svg mode change 100755 => 100644 resources/icons/document-save-as.svg mode change 100755 => 100644 resources/icons/document-save.svg mode change 100755 => 100644 resources/icons/edit-copy.svg mode change 100755 => 100644 resources/icons/edit-delete.svg mode change 100755 => 100644 resources/icons/folder-new.svg mode change 100755 => 100644 resources/icons/folder.svg mode change 100755 => 100644 resources/icons/internet-web-browser.svg mode change 100755 => 100644 resources/icons/text-x-generic.svg rename resources/{qt-bw.svg => loxodo-qt-bw.svg} (100%) mode change 100755 => 100644 rename resources/{loxodo-qt.ico => loxodo-qt-icon.ico} (100%) mode change 100755 => 100644 create mode 100644 resources/loxodo-qt-icon.svg mode change 100755 => 100644 resources/loxodo-qt.svg delete mode 100755 resources/qt.svg diff --git a/Makefile b/Makefile index a1074bc..0ae1515 100644 --- a/Makefile +++ b/Makefile @@ -11,11 +11,14 @@ exe: rm -fr build dist python setup.py py2exe -sdist: - python setup.py sdist -d .. +builddeb: + python setup.py sdist --dist-dir=../ + rename -f 's/loxodo-(.*)\.tar\.gz/loxodo_$$1\.orig\.tar\.gz/' ../* + debuild -us -uc xdg: xdg-desktop-menu install --mode system --novendor loxodo.desktop clean: - rm -fr build loxodo.egg-info + rm -fr build loxodo.egg-info debian/*.substvars debian/*.log debian/files debian/loxodo debian/loxodo-qt4 debian/loxodo-wx + diff --git a/__main__.py b/__main__.py new file mode 100644 index 0000000..b34c3d1 --- /dev/null +++ b/__main__.py @@ -0,0 +1 @@ +import loxodo diff --git a/debian/control b/debian/control index a18aa0c..29dc148 100644 --- a/debian/control +++ b/debian/control @@ -1,12 +1,22 @@ Source: loxodo -Maintainer: Christoph Sommer Section: utils Priority: optional -Build-Depends: python-setuptools (>= 0.6), python (>= 2.7), python-qt4 (>= 4.10), debhelper (>= 7.4.3), xdg-utils (>= 1.1) +Maintainer: Christoph Sommer +Build-Depends: python-setuptools (>= 0.6), python (>= 2.7), debhelper (>= 7.4.3), xdg-utils (>= 1.1) Standards-Version: 3.9.4 X-Python-Version: >= 2.7 -Package: loxodo + +Package: loxodo-wx +Architecture: all +Depends: ${misc:Depends}, ${python:Depends}, python-wxgtk2.8 (>= 2.8) +Description: Password Safe V3 compatible Password Vault + wxWidgets front-end. + + +Package: loxodo-qt4 Architecture: all -Depends: ${misc:Depends}, ${python:Depends} +Depends: ${misc:Depends}, ${python:Depends}, python-qt4 (>= 4.10) Description: Password Safe V3 compatible Password Vault + PyQT4 front-end. + diff --git a/debian/loxodo.desktop b/debian/loxodo-qt4.desktop similarity index 57% rename from debian/loxodo.desktop rename to debian/loxodo-qt4.desktop index 367e523..59b6387 100644 --- a/debian/loxodo.desktop +++ b/debian/loxodo-qt4.desktop @@ -1,10 +1,12 @@ [Desktop Entry] Name=Loxodo -Comment=Password Safe V3 compatible Password Vault +Name[en]=Loxodo +Comment=Password Safe V3 compatible Password Vault, PyQt4 front-end. Keywords=encryption;security; -Exec=/usr/bin/loxodo +Exec=/usr/bin/loxodo -qt4 Terminal=false Type=Application Icon=loxodo-qt Categories=Utility;Security; StartupNotify=true + diff --git a/debian/loxodo-qt4.docs b/debian/loxodo-qt4.docs new file mode 100644 index 0000000..1e3a42b --- /dev/null +++ b/debian/loxodo-qt4.docs @@ -0,0 +1,3 @@ +LICENSE.txt +README.txt + diff --git a/debian/loxodo-qt4.install b/debian/loxodo-qt4.install new file mode 100644 index 0000000..a96de6d --- /dev/null +++ b/debian/loxodo-qt4.install @@ -0,0 +1,2 @@ +debian/loxodo-qt4.desktop /usr/share/applications +resources/loxodo-qt-icon.svg /usr/share/pixmaps diff --git a/debian/links b/debian/loxodo-qt4.links similarity index 100% rename from debian/links rename to debian/loxodo-qt4.links diff --git a/debian/loxodo-wx.desktop b/debian/loxodo-wx.desktop new file mode 100644 index 0000000..5d252f8 --- /dev/null +++ b/debian/loxodo-wx.desktop @@ -0,0 +1,12 @@ +[Desktop Entry] +Name=Loxodo +Name[en]=Loxodo +Comment=Password Safe V3 compatible Password Vault, wxWidgets front-end. +Keywords=encryption;security; +Exec=/usr/bin/loxodo -wx +Terminal=false +Type=Application +Icon=loxodo-qt +Categories=Utility;Security; +StartupNotify=true + diff --git a/debian/loxodo-wx.docs b/debian/loxodo-wx.docs new file mode 100644 index 0000000..1e3a42b --- /dev/null +++ b/debian/loxodo-wx.docs @@ -0,0 +1,3 @@ +LICENSE.txt +README.txt + diff --git a/debian/loxodo-wx.install b/debian/loxodo-wx.install new file mode 100644 index 0000000..72cd4fb --- /dev/null +++ b/debian/loxodo-wx.install @@ -0,0 +1,2 @@ +debian/loxodo-wx.desktop /usr/share/applications +resources/loxodo-icon.svg /usr/share/pixmaps diff --git a/debian/loxodo-wx.links b/debian/loxodo-wx.links new file mode 100644 index 0000000..bfadc13 --- /dev/null +++ b/debian/loxodo-wx.links @@ -0,0 +1 @@ +/usr/share/loxodo/loxodo.py /usr/bin/loxodo diff --git a/debian/loxodo.install b/debian/loxodo.install deleted file mode 100644 index 9b6ab81..0000000 --- a/debian/loxodo.install +++ /dev/null @@ -1,2 +0,0 @@ -debian/loxodo.desktop /usr/share/applications -resources/loxodo-qt.svg /usr/share/pixmaps diff --git a/debian/rules b/debian/rules old mode 100644 new mode 100755 index 5e3ab04..fccbae4 --- a/debian/rules +++ b/debian/rules @@ -6,7 +6,8 @@ export DH_VERBOSE=1 override_dh_auto_install: $(MAKE) -C locale - python setup.py install --root=debian/loxodo --install-layout=deb --install-lib=/usr/share/loxodo --install-scripts=/usr/share/loxodo --install-data=/usr/share/loxodo -# $(MAKE) xdg + python setup.py install --root=debian/loxodo-qt4 --install-layout=deb --install-lib=/usr/share/loxodo --install-scripts=/usr/share/loxodo --install-data=/usr/share/loxodo + python setup.py install --root=debian/loxodo-wx --install-layout=deb --install-lib=/usr/share/loxodo --install-scripts=/usr/share/loxodo --install-data=/usr/share/loxodo override_dh_auto_build: + diff --git a/locale/loxodo.pot b/locale/loxodo.pot index db2c038..25c7c93 100644 --- a/locale/loxodo.pot +++ b/locale/loxodo.pot @@ -8,258 +8,286 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2008-11-16 01:48+0100\n" +"POT-Creation-Date: 2014-08-05 17:10+0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" +"Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" -#: ../src/frontends/wx/loadframe.py:41 ../src/frontends/wx/loadframe.py:90 -#: ../src/frontends/wx/vaultframe.py:384 -msgid "Vault" +#: ../src/frontends/wx/loadframe.py:39 ../src/frontends/wx/recordframe.py:48 +#: ../src/frontends/wx/vaultframe.py:423 +msgid "Password" msgstr "" -#: ../src/frontends/wx/loadframe.py:44 ../src/frontends/wx/recordframe.py:58 -#: ../src/frontends/wx/vaultframe.py:392 -msgid "Password" +#: ../src/frontends/wx/loadframe.py:42 ../src/frontends/wx/loadframe.py:88 +#: ../src/frontends/wx/vaultframe.py:415 +msgid "Vault" msgstr "" -#: ../src/frontends/wx/loadframe.py:48 +#: ../src/frontends/wx/loadframe.py:47 msgid "Open Vault" msgstr "" -#: ../src/frontends/wx/loadframe.py:90 ../src/frontends/wx/vaultframe.py:384 +#: ../src/frontends/wx/loadframe.py:88 ../src/frontends/wx/vaultframe.py:415 msgid "All files" msgstr "" -#: ../src/frontends/wx/loadframe.py:91 +#: ../src/frontends/wx/loadframe.py:89 msgid "Save new Vault as..." msgstr "" -#: ../src/frontends/wx/loadframe.py:101 +#: ../src/frontends/wx/loadframe.py:99 msgid "" "A new Vault has been created using the given password. You can now proceed " "to open the Vault." msgstr "" -#: ../src/frontends/wx/loadframe.py:102 +#: ../src/frontends/wx/loadframe.py:100 msgid "Vault Created" msgstr "" -#: ../src/frontends/wx/loadframe.py:124 ../src/frontends/wx/vaultframe.py:406 +#: ../src/frontends/wx/loadframe.py:121 ../src/frontends/wx/vaultframe.py:437 msgid "The given password does not match the Vault" msgstr "" -#: ../src/frontends/wx/loadframe.py:125 ../src/frontends/wx/vaultframe.py:372 -#: ../src/frontends/wx/vaultframe.py:407 +#: ../src/frontends/wx/loadframe.py:122 ../src/frontends/wx/vaultframe.py:403 +#: ../src/frontends/wx/vaultframe.py:438 msgid "Bad Password" msgstr "" -#: ../src/frontends/wx/loadframe.py:135 ../src/frontends/wx/vaultframe.py:415 +#: ../src/frontends/wx/loadframe.py:132 ../src/frontends/wx/vaultframe.py:446 msgid "This is not a PasswordSafe V3 Vault" msgstr "" -#: ../src/frontends/wx/loadframe.py:136 ../src/frontends/wx/loadframe.py:145 -#: ../src/frontends/wx/vaultframe.py:416 ../src/frontends/wx/vaultframe.py:425 +#: ../src/frontends/wx/loadframe.py:133 ../src/frontends/wx/loadframe.py:142 +#: ../src/frontends/wx/vaultframe.py:447 ../src/frontends/wx/vaultframe.py:456 msgid "Bad Vault" msgstr "" -#: ../src/frontends/wx/loadframe.py:144 ../src/frontends/wx/vaultframe.py:424 +#: ../src/frontends/wx/loadframe.py:141 ../src/frontends/wx/vaultframe.py:455 msgid "Vault integrity check failed" msgstr "" -#: ../src/frontends/wx/mergeframe.py:37 +#: ../src/frontends/wx/mergeframe.py:36 msgid "Select the Records to merge into this Vault" msgstr "" -#: ../src/frontends/wx/mergeframe.py:63 +#: ../src/frontends/wx/mergeframe.py:62 msgid "Merge Vault Records" msgstr "" -#: ../src/frontends/wx/recordframe.py:55 ../src/frontends/wx/vaultframe.py:47 -msgid "Group" +#: ../src/frontends/wx/recordframe.py:45 ../src/frontends/wx/vaultframe.py:44 +msgid "Title" msgstr "" -#: ../src/frontends/wx/recordframe.py:56 ../src/frontends/wx/vaultframe.py:45 -msgid "Title" +#: ../src/frontends/wx/recordframe.py:46 ../src/frontends/wx/vaultframe.py:46 +msgid "Group" msgstr "" -#: ../src/frontends/wx/recordframe.py:57 ../src/frontends/wx/vaultframe.py:46 +#: ../src/frontends/wx/recordframe.py:47 ../src/frontends/wx/vaultframe.py:45 msgid "Username" msgstr "" -#: ../src/frontends/wx/recordframe.py:59 +#: ../src/frontends/wx/recordframe.py:49 msgid "URL" msgstr "" -#: ../src/frontends/wx/recordframe.py:60 +#: ../src/frontends/wx/recordframe.py:50 msgid "Notes" msgstr "" -#: ../src/frontends/wx/recordframe.py:85 +#: ../src/frontends/wx/recordframe.py:72 msgid "Edit Vault Record" msgstr "" -#: ../src/frontends/wx/recordframe.py:114 +#: ../src/frontends/wx/recordframe.py:101 msgid "(un)mask" msgstr "" -#: ../src/frontends/wx/recordframe.py:117 +#: ../src/frontends/wx/recordframe.py:104 msgid "generate" msgstr "" -#: ../src/frontends/wx/vaultframe.py:138 +#: ../src/frontends/wx/settings.py:47 +msgid "Frontend" +msgstr "" + +#: ../src/frontends/wx/settings.py:49 +msgid "Search inside notes" +msgstr "" + +#: ../src/frontends/wx/settings.py:50 +msgid "Search inside passwords" +msgstr "" + +#: ../src/frontends/wx/settings.py:52 +msgid "Generated Password Length" +msgstr "" + +#: ../src/frontends/wx/settings.py:56 +msgid "Avoid easy to mistake chars" +msgstr "" + +#: ../src/frontends/wx/settings.py:58 +msgid "Alphabet" +msgstr "" + +#: ../src/frontends/wx/settings.py:79 +msgid "Settings" +msgstr "" + +#: ../src/frontends/wx/vaultframe.py:158 msgid "Change &Password" msgstr "" -#: ../src/frontends/wx/vaultframe.py:141 +#: ../src/frontends/wx/vaultframe.py:161 msgid "&Merge Records from" msgstr "" -#: ../src/frontends/wx/vaultframe.py:143 +#: ../src/frontends/wx/vaultframe.py:163 msgid "&About" msgstr "" -#: ../src/frontends/wx/vaultframe.py:146 +#: ../src/frontends/wx/vaultframe.py:165 +msgid "&Settings" +msgstr "" + +#: ../src/frontends/wx/vaultframe.py:168 msgid "E&xit" msgstr "" -#: ../src/frontends/wx/vaultframe.py:149 +#: ../src/frontends/wx/vaultframe.py:171 msgid "&Add\tCtrl+A" msgstr "" -#: ../src/frontends/wx/vaultframe.py:151 +#: ../src/frontends/wx/vaultframe.py:173 msgid "&Delete\tCtrl+Back" msgstr "" -#: ../src/frontends/wx/vaultframe.py:154 +#: ../src/frontends/wx/vaultframe.py:176 msgid "&Edit\tCtrl+E" msgstr "" -#: ../src/frontends/wx/vaultframe.py:158 +#: ../src/frontends/wx/vaultframe.py:180 msgid "Copy &Username\tCtrl+U" msgstr "" -#: ../src/frontends/wx/vaultframe.py:161 +#: ../src/frontends/wx/vaultframe.py:183 msgid "Copy &Password\tCtrl+P" msgstr "" -#: ../src/frontends/wx/vaultframe.py:164 -msgid "Copy UR&L\tCtrl+L" +#: ../src/frontends/wx/vaultframe.py:186 +msgid "Open UR&L\tCtrl+L" msgstr "" -#: ../src/frontends/wx/vaultframe.py:167 +#: ../src/frontends/wx/vaultframe.py:189 msgid "&Vault" msgstr "" -#: ../src/frontends/wx/vaultframe.py:168 +#: ../src/frontends/wx/vaultframe.py:190 msgid "&Record" msgstr "" -#: ../src/frontends/wx/vaultframe.py:171 +#: ../src/frontends/wx/vaultframe.py:193 msgid "Vault Contents" msgstr "" -#: ../src/frontends/wx/vaultframe.py:228 +#: ../src/frontends/wx/vaultframe.py:244 msgid "Read Vault contents from disk" msgstr "" -#: ../src/frontends/wx/vaultframe.py:239 +#: ../src/frontends/wx/vaultframe.py:255 msgid "Wrote Vault contents to disk" msgstr "" -#: ../src/frontends/wx/vaultframe.py:242 +#: ../src/frontends/wx/vaultframe.py:258 msgid "Could not write Vault contents to disk" msgstr "" -#: ../src/frontends/wx/vaultframe.py:243 +#: ../src/frontends/wx/vaultframe.py:259 msgid "Error writing to disk" msgstr "" -#: ../src/frontends/wx/vaultframe.py:252 ../src/frontends/wx/vaultframe.py:265 +#: ../src/frontends/wx/vaultframe.py:268 ../src/frontends/wx/vaultframe.py:281 msgid "Could not open clipboard" msgstr "" -#: ../src/frontends/wx/vaultframe.py:261 +#: ../src/frontends/wx/vaultframe.py:277 msgid "Cleared clipboard" msgstr "" -#: ../src/frontends/wx/vaultframe.py:300 +#: ../src/frontends/wx/vaultframe.py:314 #, python-format msgid "Changed title of \"%s\"" msgstr "" -#: ../src/frontends/wx/vaultframe.py:351 +#: ../src/frontends/wx/vaultframe.py:382 msgid "New password" msgstr "" -#: ../src/frontends/wx/vaultframe.py:352 ../src/frontends/wx/vaultframe.py:362 +#: ../src/frontends/wx/vaultframe.py:383 ../src/frontends/wx/vaultframe.py:393 msgid "Change Vault Password" msgstr "" -#: ../src/frontends/wx/vaultframe.py:361 +#: ../src/frontends/wx/vaultframe.py:392 msgid "Re-enter new password" msgstr "" -#: ../src/frontends/wx/vaultframe.py:371 +#: ../src/frontends/wx/vaultframe.py:402 msgid "The given passwords do not match" msgstr "" -#: ../src/frontends/wx/vaultframe.py:380 +#: ../src/frontends/wx/vaultframe.py:411 msgid "Changed Vault password" msgstr "" -#: ../src/frontends/wx/vaultframe.py:385 ../src/frontends/wx/vaultframe.py:393 +#: ../src/frontends/wx/vaultframe.py:416 ../src/frontends/wx/vaultframe.py:424 msgid "Open Vault..." msgstr "" -#: ../src/frontends/wx/vaultframe.py:444 +#: ../src/frontends/wx/vaultframe.py:474 msgid "new" msgstr "" -#: ../src/frontends/wx/vaultframe.py:449 +#: ../src/frontends/wx/vaultframe.py:479 #, python-format msgid "updates \"%s\"" msgstr "" -#: ../src/frontends/wx/vaultframe.py:521 +#: ../src/frontends/wx/vaultframe.py:541 msgid "" "Are you sure you want to delete this record? It contains a username or " "password and there is no way to undo this action." msgstr "" -#: ../src/frontends/wx/vaultframe.py:522 +#: ../src/frontends/wx/vaultframe.py:542 msgid "Really delete record?" msgstr "" -#: ../src/frontends/wx/vaultframe.py:546 +#: ../src/frontends/wx/vaultframe.py:563 #, python-format msgid "Copied username of \"%s\" to clipboard" msgstr "" -#: ../src/frontends/wx/vaultframe.py:548 +#: ../src/frontends/wx/vaultframe.py:565 #, python-format msgid "Error copying username of \"%s\" to clipboard" msgstr "" -#: ../src/frontends/wx/vaultframe.py:560 +#: ../src/frontends/wx/vaultframe.py:577 #, python-format msgid "Copied password of \"%s\" to clipboard" msgstr "" -#: ../src/frontends/wx/vaultframe.py:562 +#: ../src/frontends/wx/vaultframe.py:579 #, python-format msgid "Error copying password of \"%s\" to clipboard" msgstr "" -#: ../src/frontends/wx/vaultframe.py:574 -#, python-format -msgid "Copied URL of \"%s\" to clipboard" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:576 +#: ../src/frontends/wx/vaultframe.py:593 #, python-format -msgid "Error copying URL of \"%s\" to clipboard" +msgid "Could not load python module \"webbrowser\" needed to open \"%s\"" msgstr "" diff --git a/loxodo.py b/loxodo.py index 9689b3c..adeebec 100755 --- a/loxodo.py +++ b/loxodo.py @@ -22,6 +22,14 @@ from src.frontends.cmdline import loxodo sys.exit() +if '-wx' in sys.argv: + from src.frontends.wx import loxodo + sys.exit() + +if '-qt4' in sys.argv: + from src.frontends.qt4 import loxodo + sys.exit() + frontend = config.frontend # invalid frontend, select first one if not frontend in config.frontends: diff --git a/resources/__init__.py b/resources/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/resources/icons/__init__.py b/resources/icons/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/resources/icons/contact-new.svg b/resources/icons/contact-new.svg old mode 100755 new mode 100644 diff --git a/resources/icons/document-new.svg b/resources/icons/document-new.svg old mode 100755 new mode 100644 diff --git a/resources/icons/document-open.svg b/resources/icons/document-open.svg old mode 100755 new mode 100644 diff --git a/resources/icons/document-properties.svg b/resources/icons/document-properties.svg old mode 100755 new mode 100644 diff --git a/resources/icons/document-save-as.svg b/resources/icons/document-save-as.svg old mode 100755 new mode 100644 diff --git a/resources/icons/document-save.svg b/resources/icons/document-save.svg old mode 100755 new mode 100644 diff --git a/resources/icons/edit-copy.svg b/resources/icons/edit-copy.svg old mode 100755 new mode 100644 diff --git a/resources/icons/edit-delete.svg b/resources/icons/edit-delete.svg old mode 100755 new mode 100644 diff --git a/resources/icons/folder-new.svg b/resources/icons/folder-new.svg old mode 100755 new mode 100644 diff --git a/resources/icons/folder.svg b/resources/icons/folder.svg old mode 100755 new mode 100644 diff --git a/resources/icons/internet-web-browser.svg b/resources/icons/internet-web-browser.svg old mode 100755 new mode 100644 diff --git a/resources/icons/text-x-generic.svg b/resources/icons/text-x-generic.svg old mode 100755 new mode 100644 diff --git a/resources/qt-bw.svg b/resources/loxodo-qt-bw.svg old mode 100755 new mode 100644 similarity index 100% rename from resources/qt-bw.svg rename to resources/loxodo-qt-bw.svg diff --git a/resources/loxodo-qt.ico b/resources/loxodo-qt-icon.ico old mode 100755 new mode 100644 similarity index 100% rename from resources/loxodo-qt.ico rename to resources/loxodo-qt-icon.ico diff --git a/resources/loxodo-qt-icon.svg b/resources/loxodo-qt-icon.svg new file mode 100644 index 0000000..f659b2d --- /dev/null +++ b/resources/loxodo-qt-icon.svg @@ -0,0 +1,529 @@ + + + + + Logo + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Logo + + + Okami + + + + + Okami + + + + + Okami + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/loxodo-qt.svg b/resources/loxodo-qt.svg old mode 100755 new mode 100644 index f659b2d..498a096 --- a/resources/loxodo-qt.svg +++ b/resources/loxodo-qt.svg @@ -10,12 +10,12 @@ xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="256" - height="256" + width="500" + height="300" id="svg2" version="1.1" inkscape:version="0.48.4 r9939" - sodipodi:docname="qt-icon.svg"> + sodipodi:docname="qt.svg"> Logo + gradientTransform="matrix(0.33364447,0,0,0.33372583,85.563761,718.05339)" /> + transform="translate(0,-752.3622)" + style="display:inline"> + @@ -208,17 +214,17 @@ sodipodi:nodetypes="cccc" inkscape:connector-curvature="0" id="path3841" - d="M 76.614355,914.24052 C 64.738872,926.63006 57.362128,980.47951 84.774321,1029.5622 80.042991,1008.089 68.588468,979.60975 97.839875,925.30353 z" - style="fill:#e8c488;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + d="m 193.23799,901.32388 c -12.80766,13.36209 -20.76345,71.43852 8.8005,124.37402 -5.10273,-23.1588 -17.45638,-53.87353 14.09115,-112.44258 z" + style="fill:#e8c488;fill-opacity:1;stroke:#000000;stroke-width:0.66737038;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + d="m 200.64051,934.85867 c -10.86962,14.98184 -18.43037,60.03316 19.7151,98.30253 -8.20749,-22.2483 -27.12919,-40.47532 -3.84757,-102.7952 z" + style="fill:#e8ce91;fill-opacity:1;stroke:#000000;stroke-width:0.66737038;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> @@ -226,11 +232,11 @@ sodipodi:nodetypes="cccc" inkscape:connector-curvature="0" id="path3849" - d="m 175.76759,917.48111 c 13.76085,10.25396 24.20291,61.02179 5.28562,113.96389 1.11493,-21.9603 7.70178,-51.94236 -30.12774,-100.66049 z" - style="fill:#eabe75;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + d="m 300.17441,904.81885 c 14.84101,11.05887 26.10275,65.81179 5.70051,122.90965 1.20245,-23.6841 8.30635,-56.01965 -32.49266,-108.56198 z" + style="fill:#eabe75;fill-opacity:1;stroke:#000000;stroke-width:0.66737038;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> @@ -238,17 +244,17 @@ sodipodi:nodetypes="cccc" inkscape:connector-curvature="0" id="path3853" - d="m 157.02568,939.02131 c 12.19636,12.07364 33.56504,61.66619 7.4497,111.45129 4.16647,-21.5899 14.87005,-50.36 -15.79572,-103.87988 z" - style="fill:#edce93;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + d="m 279.96131,928.04989 c 13.15374,13.02137 36.19977,66.50674 8.03449,120.19971 4.49351,-23.2846 16.03729,-54.31296 -17.03563,-112.03397 z" + style="fill:#edce93;fill-opacity:1;stroke:#000000;stroke-width:0.66737038;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> @@ -256,58 +262,58 @@ sodipodi:nodetypes="cccc" inkscape:connector-curvature="0" id="path3839" - d="M 65.726728,936.09537 C 53.297739,955.86343 43.862056,982.05101 53.774314,1018.0344 54.202284,999.94228 50.25498,981.41624 72.27749,948.04659 z" - style="fill:#f8ecc6;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + d="m 181.49573,924.89426 c -13.40462,21.3198 -23.58097,49.563 -12.89063,88.37094 0.46156,-19.51227 -3.7956,-39.49254 19.9556,-75.48158 z" + style="fill:#f8ecc6;fill-opacity:1;stroke:#000000;stroke-width:0.66737038;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + d="m 173.81698,926.60293 c -7.01594,24.18797 -6.493,72.59338 14.46942,106.95557 -4.64299,-20.8865 -12.06242,-65.25851 -0.11798,-99.54497 z" + style="fill:#f4efc6;fill-opacity:1;stroke:#000000;stroke-width:0.66737038;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + d="m 206.90631,869.28806 c -17.12467,7.01886 -58.82342,47.66893 -53.26131,108.04825 4.72501,-23.23882 5.8855,-56.32557 58.46828,-97.06582 z" + style="fill:#f2ce8f;fill-opacity:1;stroke:#000000;stroke-width:0.66737038;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + d="m 302.39372,836.48401 c 57.19958,26.75334 56.82665,109.99304 25.37198,151.40711 6.38641,-10.66367 28.67285,-91.91536 -36.07023,-150.56505 z" + style="fill:#dbb779;fill-opacity:1;stroke:#000000;stroke-width:0.66737038;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + d="m 327.35953,907.34576 c 11.12663,14.79185 30.65647,69.87499 -4.99621,118.91454 7.82215,-22.3867 23.74281,-51.41231 -0.61124,-113.32078 z" + style="fill:#f0dab3;fill-opacity:1;stroke:#000000;stroke-width:0.66737038;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + d="m 186.96089,826.10844 c -47.82289,28.84278 -31.97304,155.8952 5.8578,187.78676 -10.46267,-21.52187 -31.04774,-68.82063 14.92143,-159.52893 z" + style="fill:#f2ce85;fill-opacity:1;stroke:#000000;stroke-width:0.66737038;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> @@ -315,11 +321,11 @@ sodipodi:nodetypes="cccc" inkscape:connector-curvature="0" id="path3801" - d="M 61.722321,844.98011 C 39.76285,910.45738 50.945942,938.90017 83.706755,957.7828 77.101255,949.66956 48.674009,910.88379 76.51261,847.68173 z" - style="fill:#f8e2b0;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + d="m 177.17699,826.62678 c -23.68321,70.617 -11.6223,101.29245 23.71013,121.65731 -7.124,-8.75011 -37.78269,-50.58042 -7.75885,-118.74361 z" + style="fill:#f8e2b0;fill-opacity:1;stroke:#000000;stroke-width:0.66737038;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> @@ -327,23 +333,23 @@ sodipodi:nodetypes="cccc" inkscape:connector-curvature="0" id="path3788" - d="m 76.214378,850.18045 c 5.949022,21.40659 18.927821,73.91032 68.280332,82.66366 -9.27521,-3.8142 -39.5315,-30.3678 -54.580068,-91.72573 z" - style="fill:#fdeec8;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + d="m 192.80663,832.23534 c 6.41599,23.08693 20.41358,79.71201 73.64008,89.15246 -10.00328,-4.11361 -42.63458,-32.75157 -58.8644,-98.92587 z" + style="fill:#fdeec8;fill-opacity:1;stroke:#000000;stroke-width:0.66737038;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> @@ -351,91 +357,91 @@ sodipodi:nodetypes="cccc" inkscape:connector-curvature="0" id="path3807" - d="m 94.113116,834.26658 c 58.390214,48.35697 54.574774,96.87227 111.811814,93.93598 -36.86832,-19.96418 -25.57873,-53.8286 -88.03523,-98.71013 z" - style="fill:#f1d099;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + d="m 212.11034,815.07228 c 62.97364,52.15281 58.85869,104.47641 120.58865,101.30963 C 292.93663,894.8506 305.11242,858.32796 237.7533,809.92338 z" + style="fill:#f1d099;fill-opacity:1;stroke:#000000;stroke-width:0.66737038;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + d="m 194.95148,832.71208 c 7.09693,17.16871 60.06393,70.91774 118.68211,60.5474 -23.45273,-5.99271 -67.23226,-10.61504 -107.71952,-65.79166 z" + style="fill:#f2bf88;fill-opacity:1;stroke:#000000;stroke-width:0.66737038;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + transform="matrix(0.27808583,0,0,0.27815366,184.23944,745.49224)" /> @@ -443,17 +449,17 @@ sodipodi:nodetypes="cscsc" inkscape:connector-curvature="0" id="path3925" - d="m 76.999677,953.52341 c -8.210464,-8.51606 -8.224701,-19.6399 -3.813871,-23.83435 6.94252,-6.60194 24.428905,-4.81495 32.100764,4.92841 -11.713225,-9.73543 -23.751542,-7.39595 -29.002123,-2.18038 -4.677525,4.64635 -5.57858,10.85591 0.71523,21.08632 z" - style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans" /> + d="m 193.65357,943.69036 c -8.85496,-9.18455 -8.06145,-20.77714 -3.30438,-25.30083 7.48748,-7.12017 25.53761,-4.78847 33.81168,5.7197 -12.63267,-10.49963 -25.88557,-9.86386 -31.5483,-4.23888 -5.0447,5.01106 -5.74685,12.78654 1.041,23.82001 z" + style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#460000;fill-opacity:1;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans" /> + d="m 183.88786,920.01502 c 0.24962,1.52129 1.75966,2.72771 4.62026,2.25479 -1.21527,0.65545 -2.63354,1.23705 -5.15452,-0.0985 0.6267,1.84808 2.29056,2.35172 4.60876,2.53108 -1.46757,0.69449 -3.64737,0.95341 -5.10784,-0.14749 0.55692,2.20226 3.2367,2.33253 4.78119,2.00598 l 1.63292,-6.60175 c -1.81774,1.40939 -3.26288,1.28788 -5.38077,0.0559 z" + style="fill:#460000;fill-opacity:1;stroke:none" /> @@ -461,67 +467,67 @@ sodipodi:nodetypes="ccc" inkscape:connector-curvature="0" id="path3931" - d="m 140.87788,931.67569 c 3.91562,-4.95784 8.04204,-9.21854 18.04935,-9.68006 -6.63446,1.77663 -12.86604,4.7541 -18.04935,9.68006 z" - style="fill:#000000;fill-opacity:1;stroke:none" /> + d="m 262.54597,920.12765 c 4.73135,-5.09282 9.57851,-9.48955 19.46617,-10.4399 -7.15524,1.91609 -13.87598,5.12728 -19.46617,10.4399 z" + style="fill:#460000;fill-opacity:1;stroke:none" /> + sodipodi:nodetypes="ccc" /> + d="m 265.8696,900.45268 c 10.97808,-3.94947 22.14952,-5.1224 33.51438,-3.51876 -11.74416,0.41933 -22.87997,1.63915 -33.51438,3.51876 z" + style="fill:#460000;fill-opacity:1;stroke:none" /> + transform="matrix(0.15802947,0,0,0.15806803,154.63377,826.56693)" /> diff --git a/resources/qt.svg b/resources/qt.svg deleted file mode 100755 index 498a096..0000000 --- a/resources/qt.svg +++ /dev/null @@ -1,535 +0,0 @@ - - - - - Logo - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - Logo - - - Okami - - - - - Okami - - - - - Okami - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/setup.py b/setup.py index dbffd81..db57fa7 100755 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ def get_data_files(data_dirs): 'Topic :: Security :: Cryptography', ], 'packages': find_packages(), - 'scripts': ['loxodo.py'], + 'scripts': ['loxodo.py', '__main__.py'], 'data_files': get_data_files(( ('.', 'resources'), ('.', 'locale'), diff --git a/src/__init__.py b/src/__init__.py index 2c3b23a..3de9dc0 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,6 +1,7 @@ # # Loxodo -- Password Safe V3 compatible Password Vault # Copyright (C) 2008 Christoph Sommer +# Copyright (C) 2014 Okami # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -16,3 +17,5 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # +if __file__ == '__main__': + from .. import loxodo diff --git a/src/frontends/qt4/loadframe.py b/src/frontends/qt4/loadframe.py index c276eea..00c96c6 100755 --- a/src/frontends/qt4/loadframe.py +++ b/src/frontends/qt4/loadframe.py @@ -18,6 +18,7 @@ # import os +import pkgutil from PyQt4 import QtGui from PyQt4.QtCore import Qt @@ -35,11 +36,10 @@ class LoadFrame(QtGui.QDialog): def __init__(self, parent=None, is_new=True): super(LoadFrame, self).__init__(parent) - path = os.path.dirname(os.path.realpath( - config.get_basescript())) logo = QtGui.QLabel(self) - logo.setPixmap(QtGui.QPixmap( - os.path.join(path, 'resources', 'qt-bw.svg'))) + qpixmap = QtGui.QPixmap() + qpixmap.loadFromData(pkgutil.get_data('resources', 'qt-bw.svg')) + logo.setPixmap(qpixmap) logo.setScaledContents(True) self._tc_passwd = QtGui.QLineEdit(self) diff --git a/src/frontends/qt4/loxodo.py b/src/frontends/qt4/loxodo.py index f51a5e5..f3ba57b 100755 --- a/src/frontends/qt4/loxodo.py +++ b/src/frontends/qt4/loxodo.py @@ -19,6 +19,7 @@ import os import platform +import pkgutil import sys from PyQt4 import Qt @@ -36,10 +37,9 @@ def main(): APPID = 'okami.loxodo.qt.1' ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(APPID) app = QtGui.QApplication(sys.argv) - cpath = os.path.dirname(os.path.realpath(config.get_basescript())) - ipath = os.path.join(cpath, 'resources', 'loxodo-qt.svg') - qicon = QtGui.QIcon(ipath) - app.setWindowIcon(qicon) + qpixmap = QtGui.QPixmap() + qpixmap.loadFromData(pkgutil.get_data('resources', 'loxodo-qt.svg')) + app.setWindowIcon(QtGui.QIcon(qpixmap)) mainframe = VaultFrame() mainframe.show() app.exec_() diff --git a/src/frontends/qt4/settings.py b/src/frontends/qt4/settings.py index ec3d151..3c6e5b5 100755 --- a/src/frontends/qt4/settings.py +++ b/src/frontends/qt4/settings.py @@ -18,6 +18,7 @@ # import os +import pkgutil from PyQt4 import QtGui from PyQt4.QtCore import Qt @@ -56,12 +57,14 @@ def icon_from_resources(name): - cpath = os.path.dirname(os.path.realpath(config.get_basescript())) - ipath = os.path.join(cpath, 'resources', 'icons', name + '.svg') - if os.path.exists(ipath): - return QtGui.QIcon.fromTheme(name, QtGui.QIcon(ipath)) - else: + try: + qpixmap = QtGui.QPixmap() + qpixmap.loadFromData(pkgutil.get_data( + 'resources.icons', name + '.svg')) + except IOError: return QtGui.QIcon.fromTheme(name) + else: + return QtGui.QIcon.fromTheme(name, QtGui.QIcon(qpixmap)) class Settings(QtGui.QDialog): From 7d3fa70323cb7e962d4443edce40205ea11a7a05 Mon Sep 17 00:00:00 2001 From: Okami Date: Mon, 25 Aug 2014 10:21:03 +0400 Subject: [PATCH 5/5] icons renamed --- .gitignore | 2 +- debian/loxodo-qt4.desktop | 2 +- debian/loxodo-wx.desktop | 2 +- locale/loxodo.pot | 293 --------------------------------- src/frontends/qt4/loadframe.py | 2 +- 5 files changed, 4 insertions(+), 297 deletions(-) delete mode 100644 locale/loxodo.pot diff --git a/.gitignore b/.gitignore index ca2b0af..f01d734 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ *.pyc .DS_Store - +locale/loxodo.pot diff --git a/debian/loxodo-qt4.desktop b/debian/loxodo-qt4.desktop index 59b6387..7b46bd1 100644 --- a/debian/loxodo-qt4.desktop +++ b/debian/loxodo-qt4.desktop @@ -6,7 +6,7 @@ Keywords=encryption;security; Exec=/usr/bin/loxodo -qt4 Terminal=false Type=Application -Icon=loxodo-qt +Icon=loxodo-qt-icon Categories=Utility;Security; StartupNotify=true diff --git a/debian/loxodo-wx.desktop b/debian/loxodo-wx.desktop index 5d252f8..f28a070 100644 --- a/debian/loxodo-wx.desktop +++ b/debian/loxodo-wx.desktop @@ -6,7 +6,7 @@ Keywords=encryption;security; Exec=/usr/bin/loxodo -wx Terminal=false Type=Application -Icon=loxodo-qt +Icon=loxodo-icon Categories=Utility;Security; StartupNotify=true diff --git a/locale/loxodo.pot b/locale/loxodo.pot deleted file mode 100644 index 25c7c93..0000000 --- a/locale/loxodo.pot +++ /dev/null @@ -1,293 +0,0 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR , YEAR. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2014-08-05 17:10+0400\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"Language: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=CHARSET\n" -"Content-Transfer-Encoding: 8bit\n" - -#: ../src/frontends/wx/loadframe.py:39 ../src/frontends/wx/recordframe.py:48 -#: ../src/frontends/wx/vaultframe.py:423 -msgid "Password" -msgstr "" - -#: ../src/frontends/wx/loadframe.py:42 ../src/frontends/wx/loadframe.py:88 -#: ../src/frontends/wx/vaultframe.py:415 -msgid "Vault" -msgstr "" - -#: ../src/frontends/wx/loadframe.py:47 -msgid "Open Vault" -msgstr "" - -#: ../src/frontends/wx/loadframe.py:88 ../src/frontends/wx/vaultframe.py:415 -msgid "All files" -msgstr "" - -#: ../src/frontends/wx/loadframe.py:89 -msgid "Save new Vault as..." -msgstr "" - -#: ../src/frontends/wx/loadframe.py:99 -msgid "" -"A new Vault has been created using the given password. You can now proceed " -"to open the Vault." -msgstr "" - -#: ../src/frontends/wx/loadframe.py:100 -msgid "Vault Created" -msgstr "" - -#: ../src/frontends/wx/loadframe.py:121 ../src/frontends/wx/vaultframe.py:437 -msgid "The given password does not match the Vault" -msgstr "" - -#: ../src/frontends/wx/loadframe.py:122 ../src/frontends/wx/vaultframe.py:403 -#: ../src/frontends/wx/vaultframe.py:438 -msgid "Bad Password" -msgstr "" - -#: ../src/frontends/wx/loadframe.py:132 ../src/frontends/wx/vaultframe.py:446 -msgid "This is not a PasswordSafe V3 Vault" -msgstr "" - -#: ../src/frontends/wx/loadframe.py:133 ../src/frontends/wx/loadframe.py:142 -#: ../src/frontends/wx/vaultframe.py:447 ../src/frontends/wx/vaultframe.py:456 -msgid "Bad Vault" -msgstr "" - -#: ../src/frontends/wx/loadframe.py:141 ../src/frontends/wx/vaultframe.py:455 -msgid "Vault integrity check failed" -msgstr "" - -#: ../src/frontends/wx/mergeframe.py:36 -msgid "Select the Records to merge into this Vault" -msgstr "" - -#: ../src/frontends/wx/mergeframe.py:62 -msgid "Merge Vault Records" -msgstr "" - -#: ../src/frontends/wx/recordframe.py:45 ../src/frontends/wx/vaultframe.py:44 -msgid "Title" -msgstr "" - -#: ../src/frontends/wx/recordframe.py:46 ../src/frontends/wx/vaultframe.py:46 -msgid "Group" -msgstr "" - -#: ../src/frontends/wx/recordframe.py:47 ../src/frontends/wx/vaultframe.py:45 -msgid "Username" -msgstr "" - -#: ../src/frontends/wx/recordframe.py:49 -msgid "URL" -msgstr "" - -#: ../src/frontends/wx/recordframe.py:50 -msgid "Notes" -msgstr "" - -#: ../src/frontends/wx/recordframe.py:72 -msgid "Edit Vault Record" -msgstr "" - -#: ../src/frontends/wx/recordframe.py:101 -msgid "(un)mask" -msgstr "" - -#: ../src/frontends/wx/recordframe.py:104 -msgid "generate" -msgstr "" - -#: ../src/frontends/wx/settings.py:47 -msgid "Frontend" -msgstr "" - -#: ../src/frontends/wx/settings.py:49 -msgid "Search inside notes" -msgstr "" - -#: ../src/frontends/wx/settings.py:50 -msgid "Search inside passwords" -msgstr "" - -#: ../src/frontends/wx/settings.py:52 -msgid "Generated Password Length" -msgstr "" - -#: ../src/frontends/wx/settings.py:56 -msgid "Avoid easy to mistake chars" -msgstr "" - -#: ../src/frontends/wx/settings.py:58 -msgid "Alphabet" -msgstr "" - -#: ../src/frontends/wx/settings.py:79 -msgid "Settings" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:158 -msgid "Change &Password" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:161 -msgid "&Merge Records from" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:163 -msgid "&About" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:165 -msgid "&Settings" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:168 -msgid "E&xit" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:171 -msgid "&Add\tCtrl+A" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:173 -msgid "&Delete\tCtrl+Back" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:176 -msgid "&Edit\tCtrl+E" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:180 -msgid "Copy &Username\tCtrl+U" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:183 -msgid "Copy &Password\tCtrl+P" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:186 -msgid "Open UR&L\tCtrl+L" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:189 -msgid "&Vault" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:190 -msgid "&Record" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:193 -msgid "Vault Contents" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:244 -msgid "Read Vault contents from disk" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:255 -msgid "Wrote Vault contents to disk" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:258 -msgid "Could not write Vault contents to disk" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:259 -msgid "Error writing to disk" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:268 ../src/frontends/wx/vaultframe.py:281 -msgid "Could not open clipboard" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:277 -msgid "Cleared clipboard" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:314 -#, python-format -msgid "Changed title of \"%s\"" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:382 -msgid "New password" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:383 ../src/frontends/wx/vaultframe.py:393 -msgid "Change Vault Password" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:392 -msgid "Re-enter new password" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:402 -msgid "The given passwords do not match" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:411 -msgid "Changed Vault password" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:416 ../src/frontends/wx/vaultframe.py:424 -msgid "Open Vault..." -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:474 -msgid "new" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:479 -#, python-format -msgid "updates \"%s\"" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:541 -msgid "" -"Are you sure you want to delete this record? It contains a username or " -"password and there is no way to undo this action." -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:542 -msgid "Really delete record?" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:563 -#, python-format -msgid "Copied username of \"%s\" to clipboard" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:565 -#, python-format -msgid "Error copying username of \"%s\" to clipboard" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:577 -#, python-format -msgid "Copied password of \"%s\" to clipboard" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:579 -#, python-format -msgid "Error copying password of \"%s\" to clipboard" -msgstr "" - -#: ../src/frontends/wx/vaultframe.py:593 -#, python-format -msgid "Could not load python module \"webbrowser\" needed to open \"%s\"" -msgstr "" diff --git a/src/frontends/qt4/loadframe.py b/src/frontends/qt4/loadframe.py index 00c96c6..0e2db41 100755 --- a/src/frontends/qt4/loadframe.py +++ b/src/frontends/qt4/loadframe.py @@ -38,7 +38,7 @@ def __init__(self, parent=None, is_new=True): logo = QtGui.QLabel(self) qpixmap = QtGui.QPixmap() - qpixmap.loadFromData(pkgutil.get_data('resources', 'qt-bw.svg')) + qpixmap.loadFromData(pkgutil.get_data('resources', 'loxodo-qt-bw.svg')) logo.setPixmap(qpixmap) logo.setScaledContents(True)